Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0920edb092 | ||
|
|
900c9772b1 | ||
|
|
b914c0e390 | ||
|
|
0f3146ff2c | ||
|
|
9370f9c3ee |
@@ -156,6 +156,16 @@ func (t *Table) Lookup(mnemonic string) (Instr, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// amd64 EVEX instructions take a .Z zeroing suffix (masking is written as
|
||||||
|
// an explicit K operand rather than a suffix); strip it so the base
|
||||||
|
// instruction is still recognised.
|
||||||
|
if t.Arch == AMD64 {
|
||||||
|
if base, ok := strings.CutSuffix(key, ".Z"); ok {
|
||||||
|
if in, found := t.instrs[base]; found {
|
||||||
|
return in, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return Instr{}, false
|
return Instr{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-10
@@ -23,16 +23,19 @@ import (
|
|||||||
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
|
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
|
||||||
// integer and shuffle/extract/permute/move set is in.
|
// integer and shuffle/extract/permute/move set is in.
|
||||||
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
||||||
code, _, labels, err := assemble(t, nil)
|
code, _, labels, _, err := assemble(t, nil)
|
||||||
return code, labels, err
|
return code, labels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkInfo carries file-level symbol context into a single-function assembly:
|
// linkInfo carries file-level symbol context into a single-function assembly:
|
||||||
// the set of static symbols a GLOBL in the same file defines. A nil link
|
// the set of static symbols a GLOBL in the same file defines. A nil link
|
||||||
// rejects SB operands outright (single-function assembly cannot resolve
|
// rejects SB operands outright (single-function assembly cannot resolve
|
||||||
// them).
|
// them). When allowExternal is set, a reference to a symbol no GLOBL in the
|
||||||
|
// file defines is recorded as an external relocation instead of failing —
|
||||||
|
// the object-file emitters resolve it at link time.
|
||||||
type linkInfo struct {
|
type linkInfo struct {
|
||||||
symbols map[string]bool
|
symbols map[string]bool
|
||||||
|
allowExternal bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// sbPatch is a function-relative static-symbol relocation: the disp32 field
|
// sbPatch is a function-relative static-symbol relocation: the disp32 field
|
||||||
@@ -45,9 +48,19 @@ type sbPatch struct {
|
|||||||
addend int64
|
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
|
// 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.
|
// patch sites (for the file-level layout to resolve), the label table and the
|
||||||
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, error) {
|
// stack-adjustment boundaries.
|
||||||
|
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, error) {
|
||||||
fi := computeFrame(t)
|
fi := computeFrame(t)
|
||||||
chain := jumpChain(t)
|
chain := jumpChain(t)
|
||||||
resolve := func(name string) string {
|
resolve := func(name string) string {
|
||||||
@@ -71,7 +84,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e
|
|||||||
case *ast.Instr:
|
case *ast.Instr:
|
||||||
sz, err := instrSize(s, fi, long[i], link)
|
sz, err := instrSize(s, fi, long[i], link)
|
||||||
if err != nil {
|
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
|
sizes[i] = sz
|
||||||
pcs[i] = pos
|
pcs[i] = pos
|
||||||
@@ -111,24 +124,42 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e
|
|||||||
// Pass 2: emit.
|
// Pass 2: emit.
|
||||||
out := append([]byte(nil), fi.prologue...)
|
out := append([]byte(nil), fi.prologue...)
|
||||||
var patches []sbPatch
|
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)
|
pos := len(fi.prologue)
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
s, ok := stmt.(*ast.Instr)
|
s, ok := stmt.(*ast.Instr)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
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)
|
code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
|
||||||
if err != nil {
|
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] {
|
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...)
|
patches = append(patches, ps...)
|
||||||
out = append(out, code...)
|
out = append(out, code...)
|
||||||
pos += len(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
|
// jumpChain precomputes jump-to-jump folding: a label whose first instruction
|
||||||
@@ -425,7 +456,9 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Op
|
|||||||
if a.Sym.Static {
|
if a.Sym.Static {
|
||||||
return nil, fmt.Errorf("undefined symbol %q", a.Sym.Name)
|
return nil, fmt.Errorf("undefined symbol %q", a.Sym.Name)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
|
if !link.allowExternal {
|
||||||
|
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sbMem{size: size, name: a.Sym.Name, addend: a.Sym.Offset}, nil
|
return sbMem{size: size, name: a.Sym.Name, addend: a.Sym.Offset}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+301
@@ -0,0 +1,301 @@
|
|||||||
|
// 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 ELF64 relocatable objects (ET_REL) from an assembled
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
elfVersion = 1
|
||||||
|
|
||||||
|
etREL = 1 // relocatable object
|
||||||
|
emX8664 = 62
|
||||||
|
|
||||||
|
shtNull = 0
|
||||||
|
shtProgbits = 1
|
||||||
|
shtSymtab = 2
|
||||||
|
shtStrtab = 3
|
||||||
|
shtRela = 4
|
||||||
|
|
||||||
|
shfWrite = 1
|
||||||
|
shfAlloc = 2
|
||||||
|
shfExecInstr = 4
|
||||||
|
|
||||||
|
stbLocal = 0
|
||||||
|
stbGlobal = 1
|
||||||
|
|
||||||
|
sttNotype = 0
|
||||||
|
sttObject = 1
|
||||||
|
sttFunc = 2
|
||||||
|
sttSection = 3
|
||||||
|
stInfoShift = 4
|
||||||
|
|
||||||
|
shnUndef = 0
|
||||||
|
|
||||||
|
rX8664PC32 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// elfSym is one symbol-table entry in construction.
|
||||||
|
type elfSym struct {
|
||||||
|
name string
|
||||||
|
info byte
|
||||||
|
shndx uint16
|
||||||
|
value uint64
|
||||||
|
size uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ELFObject returns the image as an ELF64 relocatable object file, ready for
|
||||||
|
// the system linker. Symbol names are the TEXT and GLOBL identifiers as
|
||||||
|
// written (the middle dot stripped); a package prefix, when present, is
|
||||||
|
// joined with a dot. Every static-symbol reference becomes an
|
||||||
|
// R_X86_64_PC32 relocation, so the code is position-independent and links
|
||||||
|
// at any address.
|
||||||
|
func (img *Image) ELFObject() ([]byte, error) {
|
||||||
|
le := binary.LittleEndian
|
||||||
|
|
||||||
|
// Section indices: 0 NULL, 1 .text, 2 .data; the tables follow.
|
||||||
|
const (
|
||||||
|
secText = 1
|
||||||
|
secData = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
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{
|
||||||
|
{}, // the mandatory null entry
|
||||||
|
{name: ".text", info: sttSection, shndx: secText},
|
||||||
|
{name: ".data", info: sttSection, shndx: secData},
|
||||||
|
}
|
||||||
|
syms = append(syms, locals...)
|
||||||
|
shInfo := len(syms) // first global symbol
|
||||||
|
syms = append(syms, globals...)
|
||||||
|
symIdx := map[string]int{}
|
||||||
|
for i, s := range syms {
|
||||||
|
symIdx[s.name] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the relocations.
|
||||||
|
type elfRela struct {
|
||||||
|
off uint64
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
relas = append(relas, elfRela{
|
||||||
|
off: uint64(fn.Offset + r.Off),
|
||||||
|
sym: idx,
|
||||||
|
// 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
|
||||||
|
// that distance with a negative sign.
|
||||||
|
addend: r.Addend - int64(r.After-r.Off),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialise the 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section presence: .rela.text only when there are relocations.
|
||||||
|
hasRela := len(relas) > 0
|
||||||
|
nSections := 6 // NULL, .text, .data, .symtab, .strtab, .shstrtab
|
||||||
|
if hasRela {
|
||||||
|
nSections = 7
|
||||||
|
}
|
||||||
|
secSymtab, secStrtab := 3, 4
|
||||||
|
secShstr := nSections - 1
|
||||||
|
|
||||||
|
// Lay the file out: header, section data, section headers.
|
||||||
|
var out []byte
|
||||||
|
out = append(out, make([]byte, 64)...) // ELF header, filled last
|
||||||
|
|
||||||
|
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 // st_other
|
||||||
|
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 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[16:], uint64(r.addend))
|
||||||
|
out = append(out, b[:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shstrOff := len(out)
|
||||||
|
out = append(out, stSections.bytes()...)
|
||||||
|
|
||||||
|
align(8)
|
||||||
|
shoff := len(out)
|
||||||
|
|
||||||
|
// Section headers.
|
||||||
|
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) // sh_addr
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||||
|
|
||||||
|
// The 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:], emX8664)
|
||||||
|
le.PutUint32(hdr[20:], elfVersion)
|
||||||
|
le.PutUint64(hdr[24:], 0) // e_entry
|
||||||
|
le.PutUint64(hdr[32:], 0) // e_phoff
|
||||||
|
le.PutUint64(hdr[40:], uint64(shoff)) // e_shoff
|
||||||
|
le.PutUint32(hdr[48:], 0) // e_flags
|
||||||
|
le.PutUint16(hdr[52:], 64) // e_ehsize
|
||||||
|
le.PutUint16(hdr[54:], 0) // e_phentsize
|
||||||
|
le.PutUint16(hdr[56:], 0) // e_phnum
|
||||||
|
le.PutUint16(hdr[58:], 64) // e_shentsize
|
||||||
|
le.PutUint16(hdr[60:], uint16(nSections))
|
||||||
|
le.PutUint16(hdr[62:], uint16(secShstr))
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectName renders a symbol's object-file name: the identifier as written,
|
||||||
|
// with an explicit package prefix joined by a dot.
|
||||||
|
func objectName(pkg, name string) string {
|
||||||
|
if pkg == "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return pkg + "." + name
|
||||||
|
}
|
||||||
|
|
||||||
|
// elfStrtab is an ELF string table under construction.
|
||||||
|
type elfStrtab struct {
|
||||||
|
buf []byte
|
||||||
|
off map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newElfStrtab() *elfStrtab {
|
||||||
|
return &elfStrtab{buf: []byte{0}, off: map[string]int{"": 0}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *elfStrtab) add(name string) {
|
||||||
|
if _, ok := s.off[name]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.off[name] = len(s.buf)
|
||||||
|
s.buf = append(s.buf, name...)
|
||||||
|
s.buf = append(s.buf, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *elfStrtab) at(name string) int { return s.off[name] }
|
||||||
|
|
||||||
|
func (s *elfStrtab) bytes() []byte { return s.buf }
|
||||||
+310
@@ -0,0 +1,310 @@
|
|||||||
|
// 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"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The object-file tests share one source: two exported functions, one
|
||||||
|
// file-local constant reached through a relocation, and one external symbol
|
||||||
|
// the linker must resolve. The functions take their arguments in the System
|
||||||
|
// V registers (not the Go stack ABI) so a C driver can call them directly.
|
||||||
|
const elfTestSrc = `
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT ·addq(SB), NOSPLIT, $0
|
||||||
|
LEAQ (DI)(SI*1), AX
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·getanswer(SB), NOSPLIT, $0
|
||||||
|
MOVQ answer<>(SB), AX
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·useextern(SB), NOSPLIT, $0
|
||||||
|
MOVQ extvar(SB), AX
|
||||||
|
RET
|
||||||
|
|
||||||
|
GLOBL answer<>(SB), RODATA, $8
|
||||||
|
DATA answer<>+0(SB)/8, $42
|
||||||
|
`
|
||||||
|
|
||||||
|
func elfTestImage(t *testing.T) *Image {
|
||||||
|
t.Helper()
|
||||||
|
f, errs := parser.Parse("t_amd64.s", elfTestSrc)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFile: %v", err)
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAssembleFileExternals checks that a reference to a symbol no GLOBL
|
||||||
|
// 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)
|
||||||
|
if len(img.Externals) != 1 || img.Externals[0] != "extvar" {
|
||||||
|
t.Fatalf("Externals = %v, want [extvar]", img.Externals)
|
||||||
|
}
|
||||||
|
var ext, local int
|
||||||
|
for _, fn := range img.Funcs {
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
if r.External {
|
||||||
|
ext++
|
||||||
|
if r.Name != "extvar" {
|
||||||
|
t.Errorf("external reloc names %q, want extvar", r.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
local++
|
||||||
|
if r.Name != "answer" {
|
||||||
|
t.Errorf("local reloc names %q, want answer", r.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ext != 1 || local != 1 {
|
||||||
|
t.Errorf("relocs = %d external, %d local; want 1 and 1", ext, local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestELFObject checks the structure of the emitted ELF64 relocatable
|
||||||
|
// object: sections, the symbol table (bindings, types, values, sizes) and
|
||||||
|
// the .rela.text relocations, parsed back with debug/elf.
|
||||||
|
func TestELFObject(t *testing.T) {
|
||||||
|
img := elfTestImage(t)
|
||||||
|
obj, err := img.ELFObject()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ELFObject: %v", err)
|
||||||
|
}
|
||||||
|
f, err := elf.NewFile(bytes.NewReader(obj))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse emitted object: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if f.Type != elf.ET_REL || f.Machine != elf.EM_X86_64 {
|
||||||
|
t.Errorf("type/machine = %v/%v, want ET_REL/EM_X86_64", f.Type, f.Machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
text := f.Section(".text")
|
||||||
|
data := f.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")
|
||||||
|
}
|
||||||
|
|
||||||
|
syms, err := f.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The emitted layout is fixed: 0 NULL, 1 .text, 2 .data.
|
||||||
|
if f.Sections[1].Name != ".text" || f.Sections[2].Name != ".data" {
|
||||||
|
t.Fatalf("section layout = %s, %s; want .text, .data", f.Sections[1].Name, f.Sections[2].Name)
|
||||||
|
}
|
||||||
|
textIdx := elf.SectionIndex(1)
|
||||||
|
dataIdx := elf.SectionIndex(2)
|
||||||
|
wantSym("addq", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 5)
|
||||||
|
wantSym("getanswer", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 8)
|
||||||
|
wantSym("useextern", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 8)
|
||||||
|
wantSym("answer", elf.STB_LOCAL, elf.STT_OBJECT, dataIdx, 8)
|
||||||
|
wantSym("extvar", elf.STB_GLOBAL, elf.STT_NOTYPE, elf.SHN_UNDEF, 0)
|
||||||
|
|
||||||
|
// Relocations: one for the file-local constant (resolving against the
|
||||||
|
// local data symbol) and one for the external (against the undefined
|
||||||
|
// global), both R_X86_64_PC32 with the −4 addend the PC-relative form
|
||||||
|
// needs. debug/elf does not surface rela entries, so read the section
|
||||||
|
// directly.
|
||||||
|
relaSec := f.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))
|
||||||
|
}
|
||||||
|
// Symbol names straight from the raw tables: r_info carries an index
|
||||||
|
// into .symtab including the null entry, which debug/elf's Symbols()
|
||||||
|
// slice may not mirror.
|
||||||
|
symtabRaw, err := f.Section(".symtab").Data()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
strtabRaw, err := f.Section(".strtab").Data()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
symName := func(idx int) string {
|
||||||
|
stName := binary.LittleEndian.Uint32(symtabRaw[idx*24:])
|
||||||
|
end := bytes.IndexByte(strtabRaw[stName:], 0)
|
||||||
|
return string(strtabRaw[stName : int(stName)+end])
|
||||||
|
}
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
e := raw[i*24 : (i+1)*24]
|
||||||
|
off := binary.LittleEndian.Uint64(e[0:])
|
||||||
|
info := binary.LittleEndian.Uint64(e[8:])
|
||||||
|
addend := int64(binary.LittleEndian.Uint64(e[16:]))
|
||||||
|
typ := info & 0xffffffff
|
||||||
|
sym := int(info >> 32)
|
||||||
|
if typ != uint64(elf.R_X86_64_PC32) {
|
||||||
|
t.Errorf("reloc %d: type %d, want R_X86_64_PC32", i, typ)
|
||||||
|
}
|
||||||
|
if addend != -4 {
|
||||||
|
t.Errorf("reloc %d: addend %d, want -4", i, addend)
|
||||||
|
}
|
||||||
|
if name := symName(sym); name != "answer" && name != "extvar" {
|
||||||
|
t.Errorf("reloc %d: symbol %q, want answer or extvar", i, name)
|
||||||
|
}
|
||||||
|
// The relocation offset lands on the disp32 field: the four bytes
|
||||||
|
// before a RET-terminated eight-byte MOVQ.
|
||||||
|
if off+4 > uint64(len(textData)) {
|
||||||
|
t.Errorf("reloc %d: offset %d outside .text", i, off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestELFObjectNoRelocations checks a file with no static-symbol references
|
||||||
|
// emits a valid object without a .rela.text section.
|
||||||
|
func TestELFObjectNoRelocations(t *testing.T) {
|
||||||
|
f, errs := parser.Parse("n_amd64.s", `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·nop(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)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func TestELFLinkAndRun(t *testing.T) {
|
||||||
|
cc, err := exec.LookPath("cc")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("no C compiler available")
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
img := elfTestImage(t)
|
||||||
|
obj, err := img.ELFObject()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ELFObject: %v", err)
|
||||||
|
}
|
||||||
|
objPath := filepath.Join(dir, "t.o")
|
||||||
|
if err := os.WriteFile(objPath, obj, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const driver = `
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
long addq(long a, long b);
|
||||||
|
long getanswer(void);
|
||||||
|
long useextern(void);
|
||||||
|
|
||||||
|
long extvar = 7;
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
printf("%ld %ld %ld\n", addq(41, 1), getanswer(), useextern());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
driverPath := filepath.Join(dir, "driver.c")
|
||||||
|
if err := os.WriteFile(driverPath, []byte(driver), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -no-pie: the encoder emits R_X86_64_PC32 for external references,
|
||||||
|
// which a position-independent executable would reject (it wants
|
||||||
|
// PLT32/GOT relocations, a future increment).
|
||||||
|
appPath := filepath.Join(dir, "app")
|
||||||
|
out, err := exec.Command(cc, "-no-pie", "-o", appPath, driverPath, objPath).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("link failed: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
run, err := exec.Command(appPath).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run failed: %v\n%s", err, run)
|
||||||
|
}
|
||||||
|
if got := string(run); got != "42 42 7\n" {
|
||||||
|
t.Errorf("output %q, want \"42 42 7\\n\"", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-5
@@ -51,9 +51,16 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
|||||||
|
|
||||||
// VEX (AVX/AVX2) and EVEX (AVX-512) instructions: the trailing
|
// 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
|
// B/W/L/Q/D is part of the mnemonic, not a size suffix, so dispatch
|
||||||
// before splitSize.
|
// before splitSize. A ".Z" suffix requests EVEX zeroing.
|
||||||
if isVex(upper) || isEvex(upper) || upper == "KMOVW" {
|
base, zeroing, err := stripEvexSuffix(upper)
|
||||||
return e.encodeVec(upper, ops)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isVex(base) || isEvex(base) || base == "KMOVW" {
|
||||||
|
return e.encodeVec(base, ops, zeroing)
|
||||||
|
}
|
||||||
|
if zeroing {
|
||||||
|
return fmt.Errorf("%s: the .Z suffix requires an EVEX instruction", mnem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CMOVcc and SETcc carry the condition in the mnemonic (CMOVLGT, SETNE).
|
// CMOVcc and SETcc carry the condition in the mnemonic (CMOVLGT, SETNE).
|
||||||
@@ -121,14 +128,20 @@ func splitSize(upper string) (base string, size int) {
|
|||||||
// its own direction-dependent opcodes; KTESTW is always VEX; everything else
|
// 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
|
// takes EVEX when an operand demands it (a ZMM or K register, or an
|
||||||
// EVEX-only mnemonic) and VEX otherwise.
|
// EVEX-only mnemonic) and VEX otherwise.
|
||||||
func (e *enc) encodeVec(upper string, ops []Operand) error {
|
func (e *enc) encodeVec(upper string, ops []Operand, zeroing bool) error {
|
||||||
if upper == "KMOVW" {
|
if upper == "KMOVW" {
|
||||||
|
if zeroing {
|
||||||
|
return fmt.Errorf("KMOVW takes no .Z suffix")
|
||||||
|
}
|
||||||
return e.encodeKmovw(ops)
|
return e.encodeKmovw(ops)
|
||||||
}
|
}
|
||||||
if upper == "KTESTW" || !evexRequired(upper, ops) {
|
if upper == "KTESTW" || !evexRequired(upper, ops) {
|
||||||
|
if zeroing {
|
||||||
|
return fmt.Errorf("%s: the .Z suffix requires an EVEX instruction", upper)
|
||||||
|
}
|
||||||
return e.encodeVex(upper, ops)
|
return e.encodeVex(upper, ops)
|
||||||
}
|
}
|
||||||
return e.encodeEvex(upper, ops)
|
return e.encodeEvex(upper, ops, zeroing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- instruction components -------------------------------------------------
|
// --- instruction components -------------------------------------------------
|
||||||
|
|||||||
+288
-34
@@ -3,14 +3,19 @@
|
|||||||
|
|
||||||
package asm
|
package asm
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// This file implements EVEX (AVX-512) instruction encoding: the four-byte
|
// This file implements EVEX (AVX-512) instruction encoding: the four-byte
|
||||||
// EVEX prefix with 5-bit vector register fields (Z0–Z31, X/Y 16–31), the
|
// EVEX prefix with 5-bit vector register fields (Z0–Z31, X/Y 16–31), the
|
||||||
// compressed disp8×N displacement, and the operand shapes the go-flac
|
// compressed disp8×N displacement, and the operand shapes the go-flac
|
||||||
// AVX-512 kernels use. Masking ({k}) and zeroing ({z}) are not supported —
|
// AVX-512 kernels use plus the common floating-point and conversion set.
|
||||||
// the kernels do not use them. K-register operands (mask destinations,
|
// Masking follows the Go assembler's spelling: an explicit K1–K7 operand
|
||||||
// KMOVW, KTESTW) are.
|
// anywhere among the operands (merging) plus a ".Z" mnemonic suffix for
|
||||||
|
// zeroing. K-register operands (mask destinations, KMOVW, KTESTW) are
|
||||||
|
// supported too.
|
||||||
|
|
||||||
// evexSpec describes one EVEX instruction's encoding parameters. The form
|
// evexSpec describes one EVEX instruction's encoding parameters. The form
|
||||||
// field reuses the vexForm shapes, which carry over unchanged.
|
// field reuses the vexForm shapes, which carry over unchanged.
|
||||||
@@ -46,6 +51,31 @@ var evexTable = map[string]evexSpec{
|
|||||||
// EVEX.128/256/512.66.0F.W1 — packed double arithmetic.
|
// EVEX.128/256/512.66.0F.W1 — packed double arithmetic.
|
||||||
"VADDPD": {1, 0x58, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VADDPD": {1, 0x58, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VMULPD": {1, 0x59, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VMULPD": {1, 0x59, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VSUBPD": {1, 0x5C, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VDIVPD": {1, 0x5E, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VMINPD": {1, 0x5D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VMAXPD": {1, 0x5F, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
// EVEX.128/256/512.66.0F.W1 — packed double unpack.
|
||||||
|
"VUNPCKLPD": {1, 0x14, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VUNPCKHPD": {1, 0x15, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
|
// EVEX.128.F2.0F.W1 — scalar double arithmetic (the packed opcodes with
|
||||||
|
// an F2 pp; the EVEX forms exist for masked and zeroing use). The
|
||||||
|
// memory operand is a single double, so disp8×N = 8.
|
||||||
|
"VADDSD": {1, 0x58, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
|
"VSUBSD": {1, 0x5C, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
|
"VMULSD": {1, 0x59, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
|
"VDIVSD": {1, 0x5E, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
|
"VMINSD": {1, 0x5D, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
|
"VMAXSD": {1, 0x5F, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
|
|
||||||
|
// EVEX.128.F3.0F.W0 — scalar single arithmetic (disp8×N = 4).
|
||||||
|
"VADDSS": {1, 0x58, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
"VSUBSS": {1, 0x5C, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
"VMULSS": {1, 0x59, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
"VDIVSS": {1, 0x5E, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
"VMINSS": {1, 0x5D, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
"VMAXSS": {1, 0x5F, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
|
||||||
// EVEX.512.66.0F3A — align (NDS + imm8).
|
// EVEX.512.66.0F3A — align (NDS + imm8).
|
||||||
"VALIGND": {3, 0x03, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VALIGND": {3, 0x03, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
@@ -59,6 +89,36 @@ var evexTable = map[string]evexSpec{
|
|||||||
// EVEX.128/256/512.F3.0F.W1 — signed qword to packed double (reg=dst,
|
// EVEX.128/256/512.F3.0F.W1 — signed qword to packed double (reg=dst,
|
||||||
// rm=src, no vvvv).
|
// rm=src, no vvvv).
|
||||||
"VCVTQQ2PD": {1, 0xE6, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
|
"VCVTQQ2PD": {1, 0xE6, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
|
// EVEX.128/256/512.F2.0F.W1 — duplicate the low double (reg=dst,
|
||||||
|
// rm=src, no vvvv): a 128-bit destination reads a single double from
|
||||||
|
// memory (disp8×8), the wider ones read the full operand.
|
||||||
|
"VMOVDDUP": {1, 0x12, 1, 3, -1, vexRM, [3]int{8, 32, 64}},
|
||||||
|
// EVEX.128/256/512.0F.W0 — signed dword to packed single (reg=dst,
|
||||||
|
// rm=src, no vvvv, no mandatory prefix — as in the VEX form).
|
||||||
|
"VCVTDQ2PS": {1, 0x5B, 0, 0, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
|
// EVEX.128/256/512.0F.W0 — packed single to packed double: the
|
||||||
|
// destination is twice the source width and sets the length; disp8×N
|
||||||
|
// follows the narrow memory source. No F3 prefix: the Go assembler
|
||||||
|
// emits this instruction with pp = 00 (Intel's maps would call that
|
||||||
|
// undefined) and gasm reproduces the Go assembler's bytes — its machine
|
||||||
|
// code is the oracle, not the manual.
|
||||||
|
"VCVTPS2PD": {1, 0x5A, 0, 0, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
|
// EVEX.128/256/512.F3.0F.W0 — signed dword to packed double (the EVEX
|
||||||
|
// form of the VEX instruction; the destination sets the length, disp8×N
|
||||||
|
// follows the narrow memory source).
|
||||||
|
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
|
// EVEX packed double → dword conversions: the source is the wide
|
||||||
|
// operand and the mnemonic fixes the length — the bare names are
|
||||||
|
// 512-bit only (ZMM source, XMM destination), the X/Y spellings are
|
||||||
|
// EVEX-128/256. Exactly one slot of n is valid; it names the vector
|
||||||
|
// length (and the disp8×N multiplier) a register or memory source
|
||||||
|
// encodes.
|
||||||
|
"VCVTPD2DQ": {1, 0xE6, 1, 3, -1, vexRMSrcLen, [3]int{0, 0, 64}},
|
||||||
|
"VCVTTPD2DQ": {1, 0xE6, 1, 1, -1, vexRMSrcLen, [3]int{0, 0, 64}},
|
||||||
|
"VCVTPD2DQX": {1, 0xE6, 1, 3, -1, vexRMSrcLen, [3]int{16, 0, 0}},
|
||||||
|
"VCVTPD2DQY": {1, 0xE6, 1, 3, -1, vexRMSrcLen, [3]int{0, 32, 0}},
|
||||||
|
"VCVTTPD2DQX": {1, 0xE6, 1, 1, -1, vexRMSrcLen, [3]int{16, 0, 0}},
|
||||||
|
"VCVTTPD2DQY": {1, 0xE6, 1, 1, -1, vexRMSrcLen, [3]int{0, 32, 0}},
|
||||||
// EVEX.128/256/512.66.0F38.W0 — sign-extend dwords to qwords; the memory
|
// EVEX.128/256/512.66.0F38.W0 — sign-extend dwords to qwords; the memory
|
||||||
// operand is the narrow source, so disp8×N follows its size (8/16/32 for
|
// operand is the narrow source, so disp8×N follows its size (8/16/32 for
|
||||||
// the xmm/ymm/zmm destination lengths).
|
// the xmm/ymm/zmm destination lengths).
|
||||||
@@ -74,6 +134,48 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPMULLQ": {2, 0x40, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPMULLQ": {2, 0x40, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3, [3]int{0, 32, 64}},
|
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3, [3]int{0, 32, 64}},
|
||||||
|
|
||||||
|
// EVEX.128/256/512 — the wider integer set (AVX-512 F/BW): byte/word
|
||||||
|
// arithmetic, the bitwise ops with D/Q suffixes, min/max, averages and
|
||||||
|
// variable shifts. All NDS form; W distinguishes element size.
|
||||||
|
"VPADDB": {1, 0xFC, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPADDW": {1, 0xFD, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSUBB": {1, 0xF8, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSUBW": {1, 0xF9, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMULLW": {1, 0xD5, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPAVGB": {1, 0xE0, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPAVGW": {1, 0xE3, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINUB": {1, 0xDA, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXUB": {1, 0xDE, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINSW": {1, 0xEA, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXSW": {1, 0xEE, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPANDD": {1, 0xDB, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPANDQ": {1, 0xDB, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPANDND": {1, 0xDF, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPANDNQ": {1, 0xDF, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINSB": {2, 0x38, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXSB": {2, 0x3C, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINSQ": {2, 0x39, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXSQ": {2, 0x3D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINUW": {2, 0x3A, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXUW": {2, 0x3E, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINSD": {2, 0x39, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXSD": {2, 0x3D, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINUD": {2, 0x3B, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXUD": {2, 0x3F, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMINUQ": {2, 0x3B, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPMAXUQ": {2, 0x3F, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSLLVD": {2, 0x47, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSLLVQ": {2, 0x47, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSRLVD": {2, 0x45, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSRLVQ": {2, 0x45, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSRAVD": {2, 0x46, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
"VPSRAVQ": {2, 0x46, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
|
// EVEX forms of instructions that also exist in VEX (selected when a ZMM
|
||||||
|
// or K register, or indices 16–31, demand EVEX).
|
||||||
|
"VPSHUFD": {1, 0x70, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
||||||
|
"VPSHUFB": {2, 0x00, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F — immediate shift (VPSLLD /6).
|
// EVEX.66.0F — immediate shift (VPSLLD /6).
|
||||||
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
|
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
@@ -117,6 +219,13 @@ var evexMoveTable = map[string]evexMoveSpec{
|
|||||||
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.F3.0F.W1 — unaligned qword move.
|
// EVEX.128/256/512.F3.0F.W1 — unaligned qword move.
|
||||||
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
||||||
|
// EVEX.128/256/512.F2.0F.W0 — unaligned byte move (byte/word moves use the
|
||||||
|
// F2 prefix, dword/qword moves F3; the element size only changes the tuple
|
||||||
|
// semantics).
|
||||||
|
"VMOVDQU8": {1, 3, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
||||||
|
// EVEX.128/256/512.F2.0F.W1 — unaligned word move (shares the qword
|
||||||
|
// encoding).
|
||||||
|
"VMOVDQU16": {1, 3, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.66.0F.W1 — unaligned packed double move.
|
// EVEX.128/256/512.66.0F.W1 — unaligned packed double move.
|
||||||
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}},
|
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}},
|
||||||
}
|
}
|
||||||
@@ -151,13 +260,75 @@ func evexRequired(upper string, ops []Operand) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeEvex encodes an EVEX instruction with operands in Plan 9 order.
|
// stripEvexSuffix splits a ".Z" zeroing suffix off the mnemonic. It is the
|
||||||
func (e *enc) encodeEvex(mnemUpper string, ops []Operand) error {
|
// only EVEX suffix supported; Go writes masking as an explicit K operand, not
|
||||||
|
// a suffix.
|
||||||
|
func stripEvexSuffix(mnem string) (base string, zeroing bool, err error) {
|
||||||
|
i := strings.LastIndexByte(mnem, '.')
|
||||||
|
if i < 0 {
|
||||||
|
return mnem, false, nil
|
||||||
|
}
|
||||||
|
if mnem[i+1:] == "Z" {
|
||||||
|
return mnem[:i], true, nil
|
||||||
|
}
|
||||||
|
return "", false, fmt.Errorf("unsupported EVEX suffix %q", mnem[i+1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitMask extracts an explicit mask register (K1–K7) from the operand list,
|
||||||
|
// returning the remaining operands and the mask index. K0 is not a usable
|
||||||
|
// mask (aaa = 0 means "no mask"), matching the assembler.
|
||||||
|
func splitMask(ops []Operand) ([]Operand, int, error) {
|
||||||
|
var rest []Operand
|
||||||
|
mask := 0
|
||||||
|
for _, op := range ops {
|
||||||
|
if r, ok := op.(Reg); ok && r.mask {
|
||||||
|
if mask != 0 {
|
||||||
|
return nil, 0, fmt.Errorf("at most one mask register operand")
|
||||||
|
}
|
||||||
|
if r.idx == 0 {
|
||||||
|
return nil, 0, fmt.Errorf("K0 is not a usable mask register")
|
||||||
|
}
|
||||||
|
mask = r.idx
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest = append(rest, op)
|
||||||
|
}
|
||||||
|
return rest, mask, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeEvex encodes an EVEX instruction with operands in Plan 9 order. The
|
||||||
|
// mask, when present, is an explicit K1–K7 operand anywhere among the
|
||||||
|
// operands; zeroing comes from the .Z mnemonic suffix and requires a mask.
|
||||||
|
func (e *enc) encodeEvex(mnemUpper string, ops []Operand, zeroing bool) error {
|
||||||
|
// Mask-destination comparisons (VPCMPEQD …, K1): the last operand is the
|
||||||
|
// destination K register, and any mask sits among the preceding operands.
|
||||||
|
if spec, ok := evexTable[mnemUpper]; ok && spec.form == vexNDS3 && len(ops) > 0 {
|
||||||
|
if dst, ok := ops[len(ops)-1].(Reg); ok && dst.mask {
|
||||||
|
rest, mask, err := splitMask(ops[:len(ops)-1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if zeroing && mask == 0 {
|
||||||
|
return fmt.Errorf("%s: zeroing (.Z) requires a mask register", mnemUpper)
|
||||||
|
}
|
||||||
|
return e.encodeEvexNDS3(spec, append(rest, dst), mask, zeroing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rest, mask, err := splitMask(ops)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if zeroing && mask == 0 {
|
||||||
|
return fmt.Errorf("%s: zeroing (.Z) requires a mask register", mnemUpper)
|
||||||
|
}
|
||||||
|
ops = rest
|
||||||
|
|
||||||
if bs, ok := evexBcastTable[mnemUpper]; ok {
|
if bs, ok := evexBcastTable[mnemUpper]; ok {
|
||||||
return e.encodeEvexBcast(bs, ops)
|
return e.encodeEvexBcast(bs, ops, mask, zeroing)
|
||||||
}
|
}
|
||||||
if ms, ok := evexMoveTable[mnemUpper]; ok {
|
if ms, ok := evexMoveTable[mnemUpper]; ok {
|
||||||
return e.encodeEvexMove(mnemUpper, ms, ops)
|
return e.encodeEvexMove(mnemUpper, ms, ops, mask, zeroing)
|
||||||
}
|
}
|
||||||
spec, ok := evexTable[mnemUpper]
|
spec, ok := evexTable[mnemUpper]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -165,17 +336,21 @@ func (e *enc) encodeEvex(mnemUpper string, ops []Operand) error {
|
|||||||
}
|
}
|
||||||
switch spec.form {
|
switch spec.form {
|
||||||
case vexNDS3:
|
case vexNDS3:
|
||||||
return e.encodeEvexNDS3(spec, ops)
|
return e.encodeEvexNDS3(spec, ops, mask, zeroing)
|
||||||
case vexRM:
|
case vexRM:
|
||||||
return e.encodeEvexRM(spec, ops)
|
return e.encodeEvexRM(spec, ops, mask, zeroing)
|
||||||
case vexRMRev:
|
case vexRMRev:
|
||||||
return e.encodeEvexRMRev(spec, ops)
|
return e.encodeEvexRMRev(spec, ops, mask, zeroing)
|
||||||
|
case vexImmRM:
|
||||||
|
return e.encodeEvexImmRM(spec, ops, mask, zeroing)
|
||||||
case vexShiftImm:
|
case vexShiftImm:
|
||||||
return e.encodeEvexShiftImm(spec, ops)
|
return e.encodeEvexShiftImm(spec, ops, mask, zeroing)
|
||||||
case vexNDS3Imm:
|
case vexNDS3Imm:
|
||||||
return e.encodeEvexNDS3Imm(spec, ops)
|
return e.encodeEvexNDS3Imm(spec, ops, mask, zeroing)
|
||||||
case vexExtract:
|
case vexExtract:
|
||||||
return e.encodeEvexExtract(spec, ops)
|
return e.encodeEvexExtract(spec, ops, mask, zeroing)
|
||||||
|
case vexRMSrcLen:
|
||||||
|
return e.encodeEvexRMSrcLen(spec, ops, mask, zeroing)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unhandled EVEX form for %s", mnemUpper)
|
return fmt.Errorf("unhandled EVEX form for %s", mnemUpper)
|
||||||
}
|
}
|
||||||
@@ -183,7 +358,7 @@ func (e *enc) encodeEvex(mnemUpper string, ops []Operand) error {
|
|||||||
// encodeEvexNDS3 encodes the three-operand NDS form: OP src2, src1, dst. The
|
// encodeEvexNDS3 encodes the three-operand NDS form: OP src2, src1, dst. The
|
||||||
// destination may be an opmask register (VPCMPEQD), in which case the vector
|
// destination may be an opmask register (VPCMPEQD), in which case the vector
|
||||||
// length comes from the sources.
|
// length comes from the sources.
|
||||||
func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand) error {
|
func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 3 {
|
if len(ops) != 3 {
|
||||||
return fmt.Errorf("EVEX NDS instruction expects 3 operands, got %d", len(ops))
|
return fmt.Errorf("EVEX NDS instruction expects 3 operands, got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -203,12 +378,12 @@ func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand) error {
|
|||||||
ll = r.vecLenBit()
|
ll = r.vecLenBit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e.emitEvexFields(spec, ll, dstReg.idx, vvvvReg.idx, src2)
|
return e.emitEvexFields(spec, ll, dstReg.idx, vvvvReg.idx, src2, mask, zeroing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeEvexRM encodes the two-operand form: OP src, dst (reg=dst, rm=src,
|
// encodeEvexRM encodes the two-operand form: OP src, dst (reg=dst, rm=src,
|
||||||
// no vvvv), e.g. VCVTQQ2PD.
|
// no vvvv), e.g. VCVTQQ2PD.
|
||||||
func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand) error {
|
func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 2 {
|
if len(ops) != 2 {
|
||||||
return fmt.Errorf("EVEX two-operand instruction expects 2 operands, got %d", len(ops))
|
return fmt.Errorf("EVEX two-operand instruction expects 2 operands, got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -217,12 +392,42 @@ func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand) error {
|
|||||||
if !ok || !dstReg.isVec() {
|
if !ok || !dstReg.isVec() {
|
||||||
return fmt.Errorf("EVEX destination must be a vector register")
|
return fmt.Errorf("EVEX destination must be a vector register")
|
||||||
}
|
}
|
||||||
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src)
|
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src, mask, zeroing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeEvexImmRM encodes the immediate shuffle form: OP $imm, src, dst
|
||||||
|
// (reg = dst, rm = src, imm8), e.g. VPSHUFD.
|
||||||
|
func (e *enc) encodeEvexImmRM(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
|
if len(ops) != 3 {
|
||||||
|
return fmt.Errorf("shuffle expects 3 operands ($imm, src, dst), got %d", len(ops))
|
||||||
|
}
|
||||||
|
imm, src, dst := ops[0], ops[1], ops[2]
|
||||||
|
immVal, ok := imm.(Imm)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("shuffle control must be an immediate")
|
||||||
|
}
|
||||||
|
dstReg, ok := dst.(Reg)
|
||||||
|
if !ok || !dstReg.isVec() {
|
||||||
|
return fmt.Errorf("shuffle destination must be a vector register")
|
||||||
|
}
|
||||||
|
ll := dstReg.vecLenBit()
|
||||||
|
if r, ok := src.(Reg); ok && r.isVec() {
|
||||||
|
ll = r.vecLenBit()
|
||||||
|
}
|
||||||
|
immByte, err := imm8(int64(immVal))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.emitEvexFields(spec, ll, dstReg.idx, -1, src, mask, zeroing); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.out = append(e.out, immByte)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeEvexShiftImm encodes an immediate shift: OP $imm, src, dst
|
// encodeEvexShiftImm encodes an immediate shift: OP $imm, src, dst
|
||||||
// (ModRM.reg = /digit, vvvv = dst, rm = src, imm8), e.g. VPSRAD $31, Z3, Z5.
|
// (ModRM.reg = /digit, vvvv = dst, rm = src, imm8), e.g. VPSRAD $31, Z3, Z5.
|
||||||
func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand) error {
|
func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 3 {
|
if len(ops) != 3 {
|
||||||
return fmt.Errorf("EVEX shift expects 3 operands ($imm, src, dst), got %d", len(ops))
|
return fmt.Errorf("EVEX shift expects 3 operands ($imm, src, dst), got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -243,7 +448,7 @@ func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), spec.opdigit, dstReg.idx, srcReg); err != nil {
|
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), spec.opdigit, dstReg.idx, srcReg, mask, zeroing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.out = append(e.out, immByte)
|
e.out = append(e.out, immByte)
|
||||||
@@ -252,7 +457,7 @@ func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand) error {
|
|||||||
|
|
||||||
// encodeEvexNDS3Imm encodes OP $imm, src2, src1, dst (reg=dst, vvvv=src1,
|
// encodeEvexNDS3Imm encodes OP $imm, src2, src1, dst (reg=dst, vvvv=src1,
|
||||||
// rm=src2, imm8), e.g. VALIGND.
|
// rm=src2, imm8), e.g. VALIGND.
|
||||||
func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand) error {
|
func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 4 {
|
if len(ops) != 4 {
|
||||||
return fmt.Errorf("instruction expects 4 operands ($imm, src2, src1, dst), got %d", len(ops))
|
return fmt.Errorf("instruction expects 4 operands ($imm, src2, src1, dst), got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -273,7 +478,7 @@ func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, vvvvReg.idx, src2); err != nil {
|
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, vvvvReg.idx, src2, mask, zeroing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.out = append(e.out, immByte)
|
e.out = append(e.out, immByte)
|
||||||
@@ -282,7 +487,7 @@ func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand) error {
|
|||||||
|
|
||||||
// encodeEvexExtract encodes OP $imm, zsrc, ydst (reg=ZMM source, rm=YMM/memory
|
// encodeEvexExtract encodes OP $imm, zsrc, ydst (reg=ZMM source, rm=YMM/memory
|
||||||
// destination, imm8), e.g. VEXTRACTI64X4.
|
// destination, imm8), e.g. VEXTRACTI64X4.
|
||||||
func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand) error {
|
func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 3 {
|
if len(ops) != 3 {
|
||||||
return fmt.Errorf("extract expects 3 operands ($imm, zsrc, ydst), got %d", len(ops))
|
return fmt.Errorf("extract expects 3 operands ($imm, zsrc, ydst), got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -299,7 +504,7 @@ func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst); err != nil {
|
if err := e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst, mask, zeroing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.out = append(e.out, immByte)
|
e.out = append(e.out, immByte)
|
||||||
@@ -309,7 +514,7 @@ func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand) error {
|
|||||||
// encodeEvexMove encodes a two-operand EVEX move; a vector→vector move uses
|
// encodeEvexMove encodes a two-operand EVEX move; a vector→vector move uses
|
||||||
// the store-form opcode (reg = source, rm = destination), matching the Go
|
// the store-form opcode (reg = source, rm = destination), matching the Go
|
||||||
// assembler.
|
// assembler.
|
||||||
func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand) error {
|
func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 2 {
|
if len(ops) != 2 {
|
||||||
return fmt.Errorf("EVEX move expects 2 operands, got %d", len(ops))
|
return fmt.Errorf("EVEX move expects 2 operands, got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -338,7 +543,47 @@ func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand) error
|
|||||||
return fmt.Errorf("%s needs a vector register operand", mnem)
|
return fmt.Errorf("%s needs a vector register operand", mnem)
|
||||||
}
|
}
|
||||||
spec := evexSpec{mapSel: ms.mapSel, opcode: op, w: ms.w, pp: ms.pp, opdigit: -1, n: ms.n}
|
spec := evexSpec{mapSel: ms.mapSel, opcode: op, w: ms.w, pp: ms.pp, opdigit: -1, n: ms.n}
|
||||||
return e.emitEvexFields(spec, reg.vecLenBit(), reg.idx, -1, rm)
|
return e.emitEvexFields(spec, reg.vecLenBit(), reg.idx, -1, rm, mask, zeroing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeEvexRMSrcLen encodes a length-narrowing conversion: OP src, dst with
|
||||||
|
// the destination always XMM and the length fixed by the mnemonic — the
|
||||||
|
// single valid slot of spec.n names the vector length (and the disp8×N
|
||||||
|
// multiplier) a register or memory source encodes.
|
||||||
|
func (e *enc) encodeEvexRMSrcLen(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
|
if len(ops) != 2 {
|
||||||
|
return fmt.Errorf("conversion expects 2 operands, got %d", len(ops))
|
||||||
|
}
|
||||||
|
src, dst := ops[0], ops[1]
|
||||||
|
dstReg, ok := dst.(Reg)
|
||||||
|
if !ok || !dstReg.isVec() {
|
||||||
|
return fmt.Errorf("EVEX destination must be a vector register")
|
||||||
|
}
|
||||||
|
ll, err := soleLen(spec.n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.emitEvexFields(spec, ll, dstReg.idx, -1, src, mask, zeroing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// soleLen returns the vector-length index of the single valid slot of n —
|
||||||
|
// the length a length-fixed mnemonic (the EVEX conversion spellings) encodes
|
||||||
|
// regardless of its operands.
|
||||||
|
func soleLen(n [3]int) (int, error) {
|
||||||
|
ll := -1
|
||||||
|
for i, v := range n {
|
||||||
|
if v == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ll >= 0 {
|
||||||
|
return 0, fmt.Errorf("ambiguous vector-length table %v", n)
|
||||||
|
}
|
||||||
|
ll = i
|
||||||
|
}
|
||||||
|
if ll < 0 {
|
||||||
|
return 0, fmt.Errorf("empty vector-length table")
|
||||||
|
}
|
||||||
|
return ll, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// memOperand reports whether op is a memory reference (including a
|
// memOperand reports whether op is a memory reference (including a
|
||||||
@@ -353,7 +598,7 @@ func memOperand(op Operand) bool {
|
|||||||
|
|
||||||
// encodeEvexRMRev encodes the narrowing-store form: OP src, dst with the wide
|
// encodeEvexRMRev encodes the narrowing-store form: OP src, dst with the wide
|
||||||
// source in the reg field and the narrow destination in r/m (VPMOVDW/QD).
|
// source in the reg field and the narrow destination in r/m (VPMOVDW/QD).
|
||||||
func (e *enc) encodeEvexRMRev(spec evexSpec, ops []Operand) error {
|
func (e *enc) encodeEvexRMRev(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 2 {
|
if len(ops) != 2 {
|
||||||
return fmt.Errorf("EVEX store instruction expects 2 operands, got %d", len(ops))
|
return fmt.Errorf("EVEX store instruction expects 2 operands, got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -362,12 +607,12 @@ func (e *enc) encodeEvexRMRev(spec evexSpec, ops []Operand) error {
|
|||||||
if !ok || !srcReg.isVec() {
|
if !ok || !srcReg.isVec() {
|
||||||
return fmt.Errorf("EVEX source must be a vector register")
|
return fmt.Errorf("EVEX source must be a vector register")
|
||||||
}
|
}
|
||||||
return e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst)
|
return e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst, mask, zeroing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeEvexBcast encodes VPBROADCASTD/Q: OP src, dst with the GPR or memory
|
// encodeEvexBcast encodes VPBROADCASTD/Q: OP src, dst with the GPR or memory
|
||||||
// source broadcast to every lane of the vector destination.
|
// source broadcast to every lane of the vector destination.
|
||||||
func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand) error {
|
func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand, mask int, zeroing bool) error {
|
||||||
if len(ops) != 2 {
|
if len(ops) != 2 {
|
||||||
return fmt.Errorf("broadcast expects 2 operands, got %d", len(ops))
|
return fmt.Errorf("broadcast expects 2 operands, got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -386,14 +631,15 @@ func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand) error {
|
|||||||
default:
|
default:
|
||||||
return fmt.Errorf("broadcast source must be a register or memory")
|
return fmt.Errorf("broadcast source must be a register or memory")
|
||||||
}
|
}
|
||||||
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src)
|
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src, mask, zeroing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// emitEvexFields emits the EVEX prefix, opcode, ModR/M, SIB and displacement
|
// emitEvexFields emits the EVEX prefix, opcode, ModR/M, SIB and displacement
|
||||||
// (disp8×N compressed) for the given precomputed fields. regIdx is the
|
// (disp8×N compressed) for the given precomputed fields. regIdx is the
|
||||||
// unextended reg-field register index, or a /digit (0–7); vvvvIdx is the
|
// unextended reg-field register index, or a /digit (0–7); vvvvIdx is the
|
||||||
// vvvv register index, or -1 when unused.
|
// vvvv register index, or -1 when unused. mask (K1–K7, 0 = unmasked) and
|
||||||
func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand) error {
|
// zeroing fill the aaa and z bits of the P2 byte.
|
||||||
|
func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand, mask int, zeroing bool) error {
|
||||||
if ll > 2 {
|
if ll > 2 {
|
||||||
return fmt.Errorf("invalid vector length")
|
return fmt.Errorf("invalid vector length")
|
||||||
}
|
}
|
||||||
@@ -420,7 +666,8 @@ func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand)
|
|||||||
var sb *sbRef
|
var sb *sbRef
|
||||||
switch r := rm.(type) {
|
switch r := rm.(type) {
|
||||||
case Reg:
|
case Reg:
|
||||||
// ModRM.mod = 11: rm[3] extends via B̄, rm[4] via X̄.
|
// ModRM.mod = 11: rm[3] extends via B̄, and rm[4] via X̄ (the EVEX
|
||||||
|
// register-register quirk).
|
||||||
modrm = 0xC0 | (regIdx&7)<<3 | (r.idx & 7)
|
modrm = 0xC0 | (regIdx&7)<<3 | (r.idx & 7)
|
||||||
sib = -1
|
sib = -1
|
||||||
if r.idx&8 != 0 {
|
if r.idx&8 != 0 {
|
||||||
@@ -429,6 +676,9 @@ func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand)
|
|||||||
if r.idx&16 != 0 {
|
if r.idx&16 != 0 {
|
||||||
xBar = 0
|
xBar = 0
|
||||||
}
|
}
|
||||||
|
if r.idx&16 != 0 {
|
||||||
|
xBar = 0
|
||||||
|
}
|
||||||
case Mem:
|
case Mem:
|
||||||
var err error
|
var err error
|
||||||
modrm, sib, disp, xBar, bBar, err = memComponentsEvex(regIdx&7, r, spec.n[ll])
|
modrm, sib, disp, xBar, bBar, err = memComponentsEvex(regIdx&7, r, spec.n[ll])
|
||||||
@@ -451,9 +701,13 @@ func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand)
|
|||||||
return fmt.Errorf("invalid EVEX r/m operand")
|
return fmt.Errorf("invalid EVEX r/m operand")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
z := 0
|
||||||
|
if zeroing {
|
||||||
|
z = 1
|
||||||
|
}
|
||||||
p0 := byte(rBar<<7 | xBar<<6 | bBar<<5 | rPrimeBar<<4 | spec.mapSel)
|
p0 := byte(rBar<<7 | xBar<<6 | bBar<<5 | rPrimeBar<<4 | spec.mapSel)
|
||||||
p1 := byte(spec.w<<7 | vBar<<3 | 1<<2 | spec.pp)
|
p1 := byte(spec.w<<7 | vBar<<3 | 1<<2 | spec.pp)
|
||||||
p2 := byte(ll<<5 | vPrimeBar<<3) // z = 0, b = 0, aaa = 0
|
p2 := byte(z<<7 | ll<<5 | vPrimeBar<<3 | mask) // z, L'L, b=0, V', aaa
|
||||||
e.out = append(e.out, 0x62, p0, p1, p2, spec.opcode, byte(modrm))
|
e.out = append(e.out, 0x62, p0, p1, p2, spec.opcode, byte(modrm))
|
||||||
if sib >= 0 {
|
if sib >= 0 {
|
||||||
e.out = append(e.out, byte(sib))
|
e.out = append(e.out, byte(sib))
|
||||||
|
|||||||
+131
-1
@@ -65,6 +65,23 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
{"VMOVDQU64 (SI)(R15*4),Z3", "VMOVDQU64", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b1fe486f1cbe"},
|
{"VMOVDQU64 (SI)(R15*4),Z3", "VMOVDQU64", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b1fe486f1cbe"},
|
||||||
{"VMOVDQU64 Z0,4(SI)(AX*1)", "VMOVDQU64", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f1fe487f840604000000"},
|
{"VMOVDQU64 Z0,4(SI)(AX*1)", "VMOVDQU64", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f1fe487f840604000000"},
|
||||||
{"VMOVDQU64 Z1,Z2", "VMOVDQU64", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487fca"},
|
{"VMOVDQU64 Z1,Z2", "VMOVDQU64", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487fca"},
|
||||||
|
// The wider AVX-512 F/BW integer set.
|
||||||
|
{"VPADDB Z1,Z2,Z3", "VPADDB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48fcd9"},
|
||||||
|
{"VPSUBW Z1,Z2,Z3", "VPSUBW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48f9d9"},
|
||||||
|
{"VPANDQ Z1,Z2,Z3", "VPANDQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed48dbd9"},
|
||||||
|
{"VPANDND Z1,Z2,Z3", "VPANDND", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48dfd9"},
|
||||||
|
{"VPMULLW Z1,Z2,Z3", "VPMULLW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48d5d9"},
|
||||||
|
{"VPMINUB Z1,Z2,Z3", "VPMINUB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48dad9"},
|
||||||
|
{"VPMAXUQ Z1,Z2,Z3", "VPMAXUQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f2ed483fd9"},
|
||||||
|
{"VPAVGW Z1,Z2,Z3", "VPAVGW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48e3d9"},
|
||||||
|
{"VPSLLVQ Z3,Z1,Z2", "VPSLLVQ", []Operand{vreg(t, "Z3"), vreg(t, "Z1"), vreg(t, "Z2")}, "62f2f54847d3"},
|
||||||
|
{"VPSRAVQ Z3,Z1,Z2", "VPSRAVQ", []Operand{vreg(t, "Z3"), vreg(t, "Z1"), vreg(t, "Z2")}, "62f2f54846d3"},
|
||||||
|
{"VPSHUFD $0x1B,Z1,Z2", "VPSHUFD", []Operand{Imm(0x1B), vreg(t, "Z1"), vreg(t, "Z2")}, "62f17d4870d11b"},
|
||||||
|
{"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.
|
||||||
|
{"VPSHUFD $1,X16,X17", "VPSHUFD", []Operand{Imm(1), vreg(t, "X16"), vreg(t, "X17")}, "62a17d0870c801"},
|
||||||
{"VMOVUPD (DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 0, 64), vreg(t, "Z14")}, "6271fd481037"},
|
{"VMOVUPD (DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 0, 64), vreg(t, "Z14")}, "6271fd481037"},
|
||||||
{"VMOVUPD 64(DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 64, 64), vreg(t, "Z14")}, "6271fd48107701"},
|
{"VMOVUPD 64(DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 64, 64), vreg(t, "Z14")}, "6271fd48107701"},
|
||||||
// Conversions and narrowing stores (reg = wide source).
|
// Conversions and narrowing stores (reg = wide source).
|
||||||
@@ -84,6 +101,32 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
{"VPBROADCASTQ AX,Z9", "VPBROADCASTQ", []Operand{AX, vreg(t, "Z9")}, "6272fd487cc8"},
|
{"VPBROADCASTQ AX,Z9", "VPBROADCASTQ", []Operand{AX, vreg(t, "Z9")}, "6272fd487cc8"},
|
||||||
// Register indices 16–31 exist only in EVEX encodings.
|
// Register indices 16–31 exist only in EVEX encodings.
|
||||||
{"VPBROADCASTD AX,Y30", "VPBROADCASTD", []Operand{AX, vreg(t, "Y30")}, "62627d287cf0"},
|
{"VPBROADCASTD AX,Y30", "VPBROADCASTD", []Operand{AX, vreg(t, "Y30")}, "62627d287cf0"},
|
||||||
|
// Packed double arithmetic / unpack (EVEX forms carry W=1).
|
||||||
|
{"VSUBPD Z1,Z2,Z3", "VSUBPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed485cd9"},
|
||||||
|
{"VDIVPD Z4,Z5,Z6", "VDIVPD", []Operand{vreg(t, "Z4"), vreg(t, "Z5"), vreg(t, "Z6")}, "62f1d5485ef4"},
|
||||||
|
{"VMINPD Z7,Z8,Z9", "VMINPD", []Operand{vreg(t, "Z7"), vreg(t, "Z8"), vreg(t, "Z9")}, "6271bd485dcf"},
|
||||||
|
{"VMAXPD Z10,Z11,Z12", "VMAXPD", []Operand{vreg(t, "Z10"), vreg(t, "Z11"), vreg(t, "Z12")}, "6251a5485fe2"},
|
||||||
|
{"VUNPCKLPD Z1,Z2,Z3", "VUNPCKLPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed4814d9"},
|
||||||
|
{"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
|
||||||
|
// 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"},
|
||||||
|
{"VMOVDDUP X16,X17", "VMOVDDUP", []Operand{vreg(t, "X16"), vreg(t, "X17")}, "62a1ff0812c8"},
|
||||||
|
// Conversions: DQ→PS, PS→PD (pp = 00, the Go assembler's choice),
|
||||||
|
// DQ→PD (the destination sets the length).
|
||||||
|
{"VCVTDQ2PS Z1,Z2", "VCVTDQ2PS", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17c485bd1"},
|
||||||
|
{"VCVTPS2PD Y1,Z2", "VCVTPS2PD", []Operand{vreg(t, "Y1"), vreg(t, "Z2")}, "62f17c485ad1"},
|
||||||
|
{"VCVTPS2PD 32(AX),Z2", "VCVTPS2PD", []Operand{Ptr(AX, 32, 32), vreg(t, "Z2")}, "62f17c485a5001"},
|
||||||
|
{"VCVTDQ2PD Y1,Z2", "VCVTDQ2PD", []Operand{vreg(t, "Y1"), vreg(t, "Z2")}, "62f17e48e6d1"},
|
||||||
|
// PD→DQ conversions: the source is the wide operand and fixes the
|
||||||
|
// length (ZMM source → L'L = 10 even with an XMM destination; a
|
||||||
|
// memory source takes the length the mnemonic's spelling implies).
|
||||||
|
{"VCVTPD2DQ Z1,Y2", "VCVTPD2DQ", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f1ff48e6d1"},
|
||||||
|
{"VCVTPD2DQ 64(AX),Y2", "VCVTPD2DQ", []Operand{Ptr(AX, 64, 64), vreg(t, "Y2")}, "62f1ff48e65001"},
|
||||||
|
{"VCVTTPD2DQ Z3,Y4", "VCVTTPD2DQ", []Operand{vreg(t, "Z3"), vreg(t, "Y4")}, "62f1fd48e6e3"},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
want := strings.ReplaceAll(c.want, " ", "")
|
want := strings.ReplaceAll(c.want, " ", "")
|
||||||
@@ -110,6 +153,93 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
mnem string
|
||||||
|
ops []Operand
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
// Masked arithmetic: K anywhere among the operands; .Z sets the z bit.
|
||||||
|
{"VPADDD.Z merging+zeroing", "VPADDD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K2"), vreg(t, "Z3")}, "62f16dcafed9"},
|
||||||
|
{"VPADDD merging", "VPADDD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f16d49fed9"},
|
||||||
|
{"VADDPD.Z", "VADDPD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K2"), vreg(t, "Z3")}, "62f1edca58d9"},
|
||||||
|
{"VPMINSD.Z", "VPMINSD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K5"), vreg(t, "Z3")}, "62f26dcd39d9"},
|
||||||
|
{"VPMINSQ.Z", "VPMINSQ.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K5"), vreg(t, "Z3")}, "62f2edcd39d9"},
|
||||||
|
// Masked immediate shift (K before the destination).
|
||||||
|
{"VPSRAD.Z", "VPSRAD.Z", []Operand{Imm(1), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f165c972e201"},
|
||||||
|
{"VPSLLD merge", "VPSLLD", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Z3")}, "62f1654a72f104"},
|
||||||
|
// Masked align.
|
||||||
|
{"VALIGND", "VALIGND", []Operand{Imm(12), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3"), vreg(t, "Z4")}, "62f36d4b03e10c"},
|
||||||
|
// Masked conversion and extract.
|
||||||
|
{"VCVTQQ2PD.Z", "VCVTQQ2PD.Z", []Operand{vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Z3")}, "62f1fecae6d9"},
|
||||||
|
{"VEXTRACTI64X4", "VEXTRACTI64X4", []Operand{Imm(1), vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Y3")}, "62f3fd4a3bcb01"},
|
||||||
|
// Masked moves: K sits between the register and memory operands.
|
||||||
|
{"VMOVDQU8 store", "VMOVDQU8", []Operand{vreg(t, "Z1"), vreg(t, "K3"), Ptr(SI, 0, 64)}, "62f17f4b7f0e"},
|
||||||
|
{"VMOVDQU32 load", "VMOVDQU32", []Operand{Ptr(SI, 0, 64), vreg(t, "K4"), vreg(t, "Z1")}, "62f17e4c6f0e"},
|
||||||
|
{"VMOVDQU32 store", "VMOVDQU32", []Operand{vreg(t, "Z1"), vreg(t, "K4"), Ptr(DI, 0, 64)}, "62f17e4c7f0f"},
|
||||||
|
// Masked comparison with a K destination: dst K1, mask K2.
|
||||||
|
{"VPCMPEQD k-dst+mask", "VPCMPEQD", []Operand{vreg(t, "Z0"), vreg(t, "Z3"), vreg(t, "K2"), vreg(t, "K1")}, "62f1654a76c8"},
|
||||||
|
// Masked floating point: packed double, the scalar SD/SS forms (which
|
||||||
|
// exist under EVEX only for masked and zeroing use) and conversions.
|
||||||
|
{"VSUBPD.Z", "VSUBPD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3"), vreg(t, "Z4")}, "62f1edcb5ce1"},
|
||||||
|
{"VADDSD merge", "VADDSD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "K3"), vreg(t, "X4")}, "62f1ef0b58e1"},
|
||||||
|
{"VSUBSD.Z", "VSUBSD.Z", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "K5"), vreg(t, "X3")}, "62f1ef8d5cd9"},
|
||||||
|
{"VADDSS merge", "VADDSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "K1"), vreg(t, "X3")}, "62f16e0958d9"},
|
||||||
|
{"VCVTPD2DQ merge", "VCVTPD2DQ", []Operand{vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Y3")}, "62f1ff4ae6d9"},
|
||||||
|
{"VCVTTPD2DQ.Z", "VCVTTPD2DQ.Z", []Operand{vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Y3")}, "62f1fdcae6d9"},
|
||||||
|
{"VCVTDQ2PS.Z", "VCVTDQ2PS.Z", []Operand{vreg(t, "Z1"), vreg(t, "K4"), vreg(t, "Z2")}, "62f17ccc5bd1"},
|
||||||
|
{"VCVTDQ2PD merge", "VCVTDQ2PD", []Operand{vreg(t, "X1"), vreg(t, "K2"), vreg(t, "X3")}, "62f17e0ae6d9"},
|
||||||
|
{"VCVTDQ2PD.Z", "VCVTDQ2PD.Z", []Operand{vreg(t, "Y1"), vreg(t, "K2"), vreg(t, "Z2")}, "62f17ecae6d1"},
|
||||||
|
{"VCVTPS2PD.Z", "VCVTPS2PD.Z", []Operand{vreg(t, "Y1"), vreg(t, "K3"), vreg(t, "Z2")}, "62f17ccb5ad1"},
|
||||||
|
}
|
||||||
|
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 := len(want) - 2; i > 0 && want[i:] == ".Z" {
|
||||||
|
want = want[:i]
|
||||||
|
}
|
||||||
|
if inst.Op.String() != want {
|
||||||
|
t.Errorf("%s: decoded as %s", c.name, inst.Op.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error cases.
|
||||||
|
bad := []struct {
|
||||||
|
name string
|
||||||
|
mnem string
|
||||||
|
ops []Operand
|
||||||
|
}{
|
||||||
|
{"zeroing without mask", "VPADDD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
|
||||||
|
{"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")}},
|
||||||
|
{"KMOVW.Z", "KMOVW.Z", []Operand{vreg(t, "K1"), vreg(t, "K2")}},
|
||||||
|
}
|
||||||
|
for _, c := range bad {
|
||||||
|
if _, err := Encode(c.mnem, c.ops...); err == nil {
|
||||||
|
t.Errorf("%s: expected an error, got none", c.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestEvexErrors checks the EVEX-specific error paths.
|
// TestEvexErrors checks the EVEX-specific error paths.
|
||||||
func TestEvexErrors(t *testing.T) {
|
func TestEvexErrors(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
@@ -125,7 +255,7 @@ func TestEvexErrors(t *testing.T) {
|
|||||||
{"VPMOVDW src", "VPMOVDW", []Operand{AX, vreg(t, "Y0")}},
|
{"VPMOVDW src", "VPMOVDW", []Operand{AX, vreg(t, "Y0")}},
|
||||||
{"align arity", "VALIGND", []Operand{Imm(1), vreg(t, "Z0"), vreg(t, "Z1")}},
|
{"align arity", "VALIGND", []Operand{Imm(1), vreg(t, "Z0"), vreg(t, "Z1")}},
|
||||||
// VEX-only mnemonics reject registers only EVEX can encode.
|
// VEX-only mnemonics reject registers only EVEX can encode.
|
||||||
{"VPSHUFD X16", "VPSHUFD", []Operand{Imm(1), vreg(t, "X16"), vreg(t, "X17")}},
|
{"VMOVMSKPS X16", "VMOVMSKPS", []Operand{vreg(t, "X16"), AX}},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
if _, err := Encode(c.mnem, c.ops...); err == nil {
|
if _, err := Encode(c.mnem, c.ops...); err == nil {
|
||||||
|
|||||||
+453
@@ -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
|
||||||
|
}
|
||||||
@@ -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 ""
|
||||||
|
}
|
||||||
+179
-43
@@ -5,27 +5,73 @@ package asm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Image is an assembled file: the function bodies laid out in source order,
|
// Image is an assembled file: the function bodies laid out in source order,
|
||||||
// followed by the file's static data section (GLOBL/DATA). Static-symbol
|
// followed by the file's static data section (GLOBL/DATA). References to
|
||||||
// references are encoded RIP-relative and resolved within the image, so the
|
// file-local static symbols are encoded RIP-relative and resolved within the
|
||||||
// bytes are self-consistent and executable at any base address.
|
// 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
|
||||||
|
// emitters turn them into linker relocations.
|
||||||
type Image struct {
|
type Image struct {
|
||||||
Code []byte // concatenated function bodies
|
Code []byte // concatenated function bodies
|
||||||
Data []byte // static data section
|
Data []byte // static data section
|
||||||
Funcs []FuncLayout // function positions, in source order
|
Funcs []FuncLayout // function positions, in source order
|
||||||
Symbols map[string]int // static symbol → byte offset within the image
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuncLayout describes one assembled function within an Image.
|
// FuncLayout describes one assembled function within an Image.
|
||||||
type FuncLayout struct {
|
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
|
||||||
|
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
|
||||||
|
// 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.
|
||||||
|
type Reloc struct {
|
||||||
|
Off int
|
||||||
|
After int
|
||||||
|
Name string
|
||||||
|
Addend int64
|
||||||
|
External bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataSymbol describes one GLOBL symbol laid out in the data section.
|
||||||
|
type DataSymbol struct {
|
||||||
Name string
|
Name string
|
||||||
Offset int // start offset within the image (== offset within Code)
|
Pkg string // explicit package prefix ("" = the current package)
|
||||||
|
Offset int // byte offset within Data
|
||||||
Size int
|
Size int
|
||||||
Labels map[string]int // local labels, function-relative
|
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.
|
// Bytes returns the whole image: code, then data.
|
||||||
@@ -37,19 +83,21 @@ func (img *Image) Bytes() []byte {
|
|||||||
|
|
||||||
// AssembleFile assembles every TEXT function of a parsed file and lays out
|
// 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
|
// its static symbols (GLOBL/DATA) in a data section behind the code. Each
|
||||||
// static-symbol reference becomes a RIP-relative load whose displacement is
|
// reference to a file-local static symbol becomes a RIP-relative load whose
|
||||||
// resolved against that layout. External (non-file-local) symbol references
|
// displacement is resolved against that layout; a reference to a symbol no
|
||||||
// are rejected: they need object-file emission.
|
// GLOBL defines is recorded as an external relocation (Externals) with its
|
||||||
|
// 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) (*Image, error) {
|
||||||
syms, order, err := collectData(f)
|
dataSyms, err := collectData(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
known := make(map[string]bool, len(syms))
|
known := make(map[string]bool, len(dataSyms))
|
||||||
for name := range syms {
|
for _, d := range dataSyms {
|
||||||
known[name] = true
|
known[d.name] = true
|
||||||
}
|
}
|
||||||
link := &linkInfo{symbols: known}
|
link := &linkInfo{symbols: known, allowExternal: true}
|
||||||
|
|
||||||
img := &Image{Symbols: map[string]int{}}
|
img := &Image{Symbols: map[string]int{}}
|
||||||
type asmFunc struct {
|
type asmFunc struct {
|
||||||
@@ -62,50 +110,100 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
code, patches, labels, err := assemble(t, link)
|
code, patches, labels, steps, err := assemble(t, link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||||
}
|
}
|
||||||
img.Funcs = append(img.Funcs, FuncLayout{
|
fl := FuncLayout{
|
||||||
Name: t.Name.Name,
|
Name: t.Name.Name,
|
||||||
|
Pkg: t.Name.Pkg,
|
||||||
|
Static: t.Name.Static,
|
||||||
Offset: len(img.Code),
|
Offset: len(img.Code),
|
||||||
Size: len(code),
|
Size: len(code),
|
||||||
|
Frame: frameSize(t),
|
||||||
|
Args: argsSize(t),
|
||||||
|
Line: t.Pos().Line,
|
||||||
Labels: labels,
|
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...)
|
img.Code = append(img.Code, code...)
|
||||||
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lay out the data section behind the code, each symbol 16-aligned.
|
// Lay out the data section behind the code, each symbol 16-aligned.
|
||||||
dataStart := len(img.Code)
|
dataStart := len(img.Code)
|
||||||
for _, name := range order {
|
for _, d := range dataSyms {
|
||||||
if pos := dataStart + len(img.Data); pos != align16(pos) {
|
if pos := dataStart + len(img.Data); pos != align16(pos) {
|
||||||
img.Data = append(img.Data, make([]byte, align16(pos)-pos)...)
|
img.Data = append(img.Data, make([]byte, align16(pos)-pos)...)
|
||||||
}
|
}
|
||||||
img.Symbols[name] = dataStart + len(img.Data)
|
img.Symbols[d.name] = dataStart + len(img.Data)
|
||||||
img.Data = append(img.Data, syms[name]...)
|
img.DataSyms = append(img.DataSyms, DataSymbol{
|
||||||
|
Name: d.name,
|
||||||
|
Pkg: d.pkg,
|
||||||
|
Offset: len(img.Data),
|
||||||
|
Size: len(d.buf),
|
||||||
|
Static: d.static,
|
||||||
|
Rodata: d.rodata,
|
||||||
|
Dupok: d.dupok,
|
||||||
|
})
|
||||||
|
img.Data = append(img.Data, d.buf...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the RIP-relative displacements now that every address is known.
|
// Resolve the RIP-relative displacements of file-local references now
|
||||||
|
// that every address is known, and record every reference (resolved or
|
||||||
|
// external) for the object-file emitters.
|
||||||
|
externals := map[string]bool{}
|
||||||
for i, fn := range funcs {
|
for i, fn := range funcs {
|
||||||
base := img.Funcs[i].Offset
|
base := img.Funcs[i].Offset
|
||||||
code := img.Code[base : base+img.Funcs[i].Size]
|
code := img.Code[base : base+img.Funcs[i].Size]
|
||||||
for _, p := range fn.patches {
|
for _, p := range fn.patches {
|
||||||
rel := int64(img.Symbols[p.name]) + p.addend - int64(base+p.after)
|
reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend}
|
||||||
if rel < -1<<31 || rel >= 1<<31 {
|
if imgOff, ok := img.Symbols[p.name]; ok {
|
||||||
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
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
|
||||||
}
|
}
|
||||||
copy(code[p.off:p.off+4], le32(rel))
|
img.Funcs[i].Relocs = append(img.Funcs[i].Relocs, reloc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for name := range externals {
|
||||||
|
img.Externals = append(img.Externals, name)
|
||||||
|
}
|
||||||
|
sort.Strings(img.Externals)
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dataSym is one GLOBL symbol and its DATA initialiser.
|
||||||
|
type dataSym struct {
|
||||||
|
name string
|
||||||
|
pkg string
|
||||||
|
buf []byte
|
||||||
|
static bool
|
||||||
|
rodata bool
|
||||||
|
dupok bool
|
||||||
|
}
|
||||||
|
|
||||||
// collectData gathers the file's static symbols (GLOBL) and their initial
|
// collectData gathers the file's static symbols (GLOBL) and their initial
|
||||||
// contents (DATA) into byte buffers, in declaration order.
|
// contents (DATA) into byte buffers, in declaration order.
|
||||||
func collectData(f *ast.File) (map[string][]byte, []string, error) {
|
func collectData(f *ast.File) ([]dataSym, error) {
|
||||||
syms := map[string][]byte{}
|
index := map[string]int{}
|
||||||
var order []string
|
var syms []dataSym
|
||||||
for _, d := range f.Decls {
|
for _, d := range f.Decls {
|
||||||
switch dd := d.(type) {
|
switch dd := d.(type) {
|
||||||
case *ast.Globl:
|
case *ast.Globl:
|
||||||
@@ -113,50 +211,88 @@ func collectData(f *ast.File) (map[string][]byte, []string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := dd.Name.Name
|
name := dd.Name.Name
|
||||||
if _, dup := syms[name]; dup {
|
if _, dup := index[name]; dup {
|
||||||
return nil, nil, fmt.Errorf("duplicate GLOBL %q", name)
|
return nil, fmt.Errorf("duplicate GLOBL %q", name)
|
||||||
}
|
}
|
||||||
size := 0
|
size := 0
|
||||||
if dd.Size != nil && dd.Size.Imm.HasVal {
|
if dd.Size != nil && dd.Size.Imm.HasVal {
|
||||||
size = int(dd.Size.Imm.Val)
|
size = int(dd.Size.Imm.Val)
|
||||||
}
|
}
|
||||||
syms[name] = make([]byte, size)
|
index[name] = len(syms)
|
||||||
order = append(order, name)
|
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:
|
case *ast.Data:
|
||||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
buf, ok := syms[dd.Name.Name]
|
i, ok := index[dd.Name.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name)
|
return nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name)
|
||||||
}
|
}
|
||||||
if dd.Value == nil || !dd.Value.Imm.HasVal {
|
if dd.Value == nil || !dd.Value.Imm.HasVal {
|
||||||
return nil, nil, fmt.Errorf("DATA %q: value must be an integer immediate", dd.Name.Name)
|
return nil, fmt.Errorf("DATA %q: value must be an integer immediate", dd.Name.Name)
|
||||||
}
|
}
|
||||||
w := dd.Width
|
w := dd.Width
|
||||||
switch w {
|
switch w {
|
||||||
case 1, 2, 4, 8:
|
case 1, 2, 4, 8:
|
||||||
default:
|
default:
|
||||||
return nil, nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w)
|
return nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w)
|
||||||
}
|
}
|
||||||
off := dd.Name.Offset
|
off := dd.Name.Offset
|
||||||
|
buf := syms[i].buf
|
||||||
if off < 0 || off+int64(w) > int64(len(buf)) {
|
if off < 0 || off+int64(w) > int64(len(buf)) {
|
||||||
return nil, nil, fmt.Errorf("DATA %q+%d/%d exceeds GLOBL size %d", dd.Name.Name, off, w, len(buf))
|
return nil, fmt.Errorf("DATA %q+%d/%d exceeds GLOBL size %d", dd.Name.Name, off, w, len(buf))
|
||||||
}
|
}
|
||||||
v := dd.Value.Imm.Val
|
v := dd.Value.Imm.Val
|
||||||
if dd.Value.Imm.Neg {
|
if dd.Value.Imm.Neg {
|
||||||
v = -v
|
v = -v
|
||||||
}
|
}
|
||||||
for i := 0; i < w; i++ {
|
for j := 0; j < w; j++ {
|
||||||
buf[off+int64(i)] = byte(v >> (8 * i))
|
buf[off+int64(j)] = byte(v >> (8 * j))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return syms, order, nil
|
return syms, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// align16 rounds n up to the next multiple of 16.
|
// align16 rounds n up to the next multiple of 16.
|
||||||
func align16(n int) int {
|
func align16(n int) int {
|
||||||
return (n + 15) &^ 15
|
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
|
||||||
|
}
|
||||||
|
|||||||
+258
@@ -0,0 +1,258 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -43,6 +43,13 @@ const (
|
|||||||
// in ModRM.reg and the destination in r/m — the layout of the EVEX
|
// in ModRM.reg and the destination in r/m — the layout of the EVEX
|
||||||
// narrowing stores (VPMOVDW, VPMOVQD).
|
// narrowing stores (VPMOVDW, VPMOVQD).
|
||||||
vexRMRev
|
vexRMRev
|
||||||
|
// vexRMSrcLen is the two-operand conversion form `OP src, dst` whose
|
||||||
|
// vector length follows the source: the packed-double → dword
|
||||||
|
// conversions (VCVTPD2DQ/VCVTTPD2DQ and their X/Y spellings) narrow into
|
||||||
|
// an XMM destination, so the L bit rides with the wider source. The
|
||||||
|
// mnemonic's spelling fixes the length (X = 128, Y = 256), which also
|
||||||
|
// covers a memory source. ModRM.reg = dst, ModRM.rm = src, no vvvv.
|
||||||
|
vexRMSrcLen
|
||||||
// vexZero is the no-operand form (VZEROUPPER).
|
// vexZero is the no-operand form (VZEROUPPER).
|
||||||
vexZero
|
vexZero
|
||||||
)
|
)
|
||||||
@@ -86,12 +93,29 @@ var vexTable = map[string]vexSpec{
|
|||||||
// VEX.128/256.66.0F.WIG — packed double-precision arithmetic / logic.
|
// VEX.128/256.66.0F.WIG — packed double-precision arithmetic / logic.
|
||||||
"VADDPD": {1, 0x58, 0, 1, -1, vexNDS3},
|
"VADDPD": {1, 0x58, 0, 1, -1, vexNDS3},
|
||||||
"VMULPD": {1, 0x59, 0, 1, -1, vexNDS3},
|
"VMULPD": {1, 0x59, 0, 1, -1, vexNDS3},
|
||||||
|
"VSUBPD": {1, 0x5C, 0, 1, -1, vexNDS3},
|
||||||
|
"VDIVPD": {1, 0x5E, 0, 1, -1, vexNDS3},
|
||||||
|
"VMINPD": {1, 0x5D, 0, 1, -1, vexNDS3},
|
||||||
|
"VMAXPD": {1, 0x5F, 0, 1, -1, vexNDS3},
|
||||||
"VXORPD": {1, 0x57, 0, 1, -1, vexNDS3},
|
"VXORPD": {1, 0x57, 0, 1, -1, vexNDS3},
|
||||||
"VUNPCKHPD": {1, 0x15, 0, 1, -1, vexNDS3},
|
"VUNPCKHPD": {1, 0x15, 0, 1, -1, vexNDS3},
|
||||||
|
"VUNPCKLPD": {1, 0x14, 0, 1, -1, vexNDS3},
|
||||||
// VEX.128.F2.0F.WIG — scalar double-precision arithmetic (the packed
|
// VEX.128.F2.0F.WIG — scalar double-precision arithmetic (the packed
|
||||||
// opcodes with an F2 pp).
|
// opcodes with an F2 pp).
|
||||||
"VADDSD": {1, 0x58, 0, 3, -1, vexNDS3},
|
"VADDSD": {1, 0x58, 0, 3, -1, vexNDS3},
|
||||||
|
"VSUBSD": {1, 0x5C, 0, 3, -1, vexNDS3},
|
||||||
"VMULSD": {1, 0x59, 0, 3, -1, vexNDS3},
|
"VMULSD": {1, 0x59, 0, 3, -1, vexNDS3},
|
||||||
|
"VDIVSD": {1, 0x5E, 0, 3, -1, vexNDS3},
|
||||||
|
"VMINSD": {1, 0x5D, 0, 3, -1, vexNDS3},
|
||||||
|
"VMAXSD": {1, 0x5F, 0, 3, -1, vexNDS3},
|
||||||
|
// VEX.128.F3.0F.WIG — scalar single-precision arithmetic (the packed
|
||||||
|
// opcodes with an F3 pp).
|
||||||
|
"VADDSS": {1, 0x58, 0, 2, -1, vexNDS3},
|
||||||
|
"VSUBSS": {1, 0x5C, 0, 2, -1, vexNDS3},
|
||||||
|
"VMULSS": {1, 0x59, 0, 2, -1, vexNDS3},
|
||||||
|
"VDIVSS": {1, 0x5E, 0, 2, -1, vexNDS3},
|
||||||
|
"VMINSS": {1, 0x5D, 0, 2, -1, vexNDS3},
|
||||||
|
"VMAXSS": {1, 0x5F, 0, 2, -1, vexNDS3},
|
||||||
// VEX.128/256.66.0F38.W1 — fused multiply-add (NDS form).
|
// VEX.128/256.66.0F38.W1 — fused multiply-add (NDS form).
|
||||||
"VFMADD231PD": {2, 0xB8, 1, 1, -1, vexNDS3},
|
"VFMADD231PD": {2, 0xB8, 1, 1, -1, vexNDS3},
|
||||||
|
|
||||||
@@ -105,6 +129,19 @@ var vexTable = map[string]vexSpec{
|
|||||||
// VEX.128/256.F3.0F.WIG — signed dword to packed double conversion
|
// VEX.128/256.F3.0F.WIG — signed dword to packed double conversion
|
||||||
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
||||||
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM},
|
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM},
|
||||||
|
// VEX.128/256.0F.WIG — signed dword to packed single conversion
|
||||||
|
// (reg=dst, rm=src, no vvvv, no mandatory prefix).
|
||||||
|
"VCVTDQ2PS": {1, 0x5B, 0, 0, -1, vexRM},
|
||||||
|
// VEX.128/256.0F.WIG — packed single to packed double conversion
|
||||||
|
// (reg=dst, rm=src; the destination is the wide operand and sets the
|
||||||
|
// length). Intel's maps prescribe the F3 prefix here (VEX.pp = 10), but
|
||||||
|
// the Go assembler emits the instruction with pp = 00, and gasm follows
|
||||||
|
// the Go assembler's bytes — its machine code is the oracle, not the
|
||||||
|
// manual.
|
||||||
|
"VCVTPS2PD": {1, 0x5A, 0, 0, -1, vexRM},
|
||||||
|
// VEX.128.F2.0F.WIG — duplicate the low double of each 128-bit lane
|
||||||
|
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
||||||
|
"VMOVDDUP": {1, 0x12, 0, 3, -1, vexRM},
|
||||||
// VEX.128/256.66.0F.WIG — move mask to a GPR (reg=gpr dst, rm=vec src).
|
// VEX.128/256.66.0F.WIG — move mask to a GPR (reg=gpr dst, rm=vec src).
|
||||||
"VPMOVMSKB": {1, 0xD7, 0, 1, -1, vexRM},
|
"VPMOVMSKB": {1, 0xD7, 0, 1, -1, vexRM},
|
||||||
"VMOVMSKPS": {1, 0x50, 0, 0, -1, vexRM}, // no 66 prefix (that would be VMOVMSKPD)
|
"VMOVMSKPS": {1, 0x50, 0, 0, -1, vexRM}, // no 66 prefix (that would be VMOVMSKPD)
|
||||||
@@ -138,6 +175,25 @@ var vexTable = map[string]vexSpec{
|
|||||||
|
|
||||||
// VEX.128.0F.W0 — mask-register test (KTESTW k1, k2: reg = dst, rm = src).
|
// VEX.128.0F.W0 — mask-register test (KTESTW k1, k2: reg = dst, rm = src).
|
||||||
"KTESTW": {1, 0x99, 0, 0, -1, vexRM},
|
"KTESTW": {1, 0x99, 0, 0, -1, vexRM},
|
||||||
|
|
||||||
|
// VEX.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.
|
||||||
|
"VCVTPD2DQX": {1, 0xE6, 0, 3, -1, vexRMSrcLen},
|
||||||
|
"VCVTPD2DQY": {1, 0xE6, 0, 3, -1, vexRMSrcLen},
|
||||||
|
"VCVTTPD2DQX": {1, 0xE6, 0, 1, -1, vexRMSrcLen},
|
||||||
|
"VCVTTPD2DQY": {1, 0xE6, 0, 1, -1, vexRMSrcLen},
|
||||||
|
}
|
||||||
|
|
||||||
|
// vexSrcLen maps a source-length conversion mnemonic (the X/Y spellings of
|
||||||
|
// the packed-double → dword conversions) to its fixed vector length:
|
||||||
|
// X = 128 (L = 0), Y = 256 (L = 1). The spelling fixes the length even for
|
||||||
|
// a memory source, matching the Go assembler's ytab.
|
||||||
|
var vexSrcLen = map[string]int{
|
||||||
|
"VCVTPD2DQX": 0,
|
||||||
|
"VCVTPD2DQY": 1,
|
||||||
|
"VCVTTPD2DQX": 0,
|
||||||
|
"VCVTTPD2DQY": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
// vexVarShift maps the shift mnemonics to their variable-count opcode — the
|
// vexVarShift maps the shift mnemonics to their variable-count opcode — the
|
||||||
@@ -230,6 +286,8 @@ func (e *enc) encodeVex(mnemUpper string, ops []Operand) error {
|
|||||||
return e.encodeVexNDS3Imm(spec, ops)
|
return e.encodeVexNDS3Imm(spec, ops)
|
||||||
case vexExtract:
|
case vexExtract:
|
||||||
return e.encodeVexExtract(spec, ops)
|
return e.encodeVexExtract(spec, ops)
|
||||||
|
case vexRMSrcLen:
|
||||||
|
return e.encodeVexRMSrcLen(mnemUpper, spec, ops)
|
||||||
case vexZero:
|
case vexZero:
|
||||||
return e.encodeVexZero(mnemUpper, spec, ops)
|
return e.encodeVexZero(mnemUpper, spec, ops)
|
||||||
}
|
}
|
||||||
@@ -295,6 +353,32 @@ func (e *enc) encodeVexRM(spec vexSpec, ops []Operand) error {
|
|||||||
return e.emitVexFields(spec, l, regField, rBit, 15, src)
|
return e.emitVexFields(spec, l, regField, rBit, 15, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeVexRMSrcLen encodes a length-narrowing conversion: OP src, dst with
|
||||||
|
// the destination always XMM and the VEX.L bit following the source — fixed
|
||||||
|
// by the mnemonic's spelling (VCVTPD2DQX = 128, VCVTPD2DQY = 256) even when
|
||||||
|
// the source is memory.
|
||||||
|
func (e *enc) encodeVexRMSrcLen(mnem string, spec vexSpec, ops []Operand) error {
|
||||||
|
if len(ops) != 2 {
|
||||||
|
return fmt.Errorf("conversion expects 2 operands, got %d", len(ops))
|
||||||
|
}
|
||||||
|
src, dst := ops[0], ops[1]
|
||||||
|
dstReg, ok := dst.(Reg)
|
||||||
|
if !ok || !dstReg.isVec() {
|
||||||
|
return fmt.Errorf("VEX destination must be a vector register")
|
||||||
|
}
|
||||||
|
ll, ok := vexSrcLen[mnem]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no fixed vector length for %s", mnem)
|
||||||
|
}
|
||||||
|
regField := dstReg.idx & 7
|
||||||
|
rBit := 0
|
||||||
|
if dstReg.idx >= 8 {
|
||||||
|
rBit = 1
|
||||||
|
}
|
||||||
|
// An unused vvvv field must be stored as all ones (v̄vvv = 1111).
|
||||||
|
return e.emitVexFields(spec, ll, regField, rBit, 15, src)
|
||||||
|
}
|
||||||
|
|
||||||
// encodeVexShiftImm encodes an immediate-shift instruction: OP $imm, src, dst.
|
// encodeVexShiftImm encodes an immediate-shift instruction: OP $imm, src, dst.
|
||||||
// The destination is carried in VEX.vvvv, the source in ModRM.rm, and the
|
// The destination is carried in VEX.vvvv, the source in ModRM.rm, and the
|
||||||
// shift kind in the ModRM.reg /digit.
|
// shift kind in the ModRM.reg /digit.
|
||||||
|
|||||||
+107
-64
@@ -156,82 +156,121 @@ func TestVexShiftImm(t *testing.T) {
|
|||||||
// as well as every new operand form.
|
// as well as every new operand form.
|
||||||
func TestVexGroundTruth(t *testing.T) {
|
func TestVexGroundTruth(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
mnem string
|
mnem string
|
||||||
ops []Operand
|
ops []Operand
|
||||||
want string
|
want string
|
||||||
|
wantOp string // decoded mnemonic, when it differs from mnem (the X/Y spellings)
|
||||||
}{
|
}{
|
||||||
// Three-operand NDS form.
|
// Three-operand NDS form.
|
||||||
{"VPADDQ Y8,Y9,Y8", "VPADDQ", []Operand{vreg(t, "Y8"), vreg(t, "Y9"), vreg(t, "Y8")}, "c44135d4c0"},
|
{"VPADDQ Y8,Y9,Y8", "VPADDQ", []Operand{vreg(t, "Y8"), vreg(t, "Y9"), vreg(t, "Y8")}, "c44135d4c0", ""},
|
||||||
{"VPADDQ X9,X8,X8", "VPADDQ", []Operand{vreg(t, "X9"), vreg(t, "X8"), vreg(t, "X8")}, "c44139d4c1"},
|
{"VPADDQ X9,X8,X8", "VPADDQ", []Operand{vreg(t, "X9"), vreg(t, "X8"), vreg(t, "X8")}, "c44139d4c1", ""},
|
||||||
{"VPXOR X7,X7,X7", "VPXOR", []Operand{vreg(t, "X7"), vreg(t, "X7"), vreg(t, "X7")}, "c5c1efff"},
|
{"VPXOR X7,X7,X7", "VPXOR", []Operand{vreg(t, "X7"), vreg(t, "X7"), vreg(t, "X7")}, "c5c1efff", ""},
|
||||||
{"VPSHUFB Y1,Y2,Y3", "VPSHUFB", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d00d9"},
|
{"VPSHUFB Y1,Y2,Y3", "VPSHUFB", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d00d9", ""},
|
||||||
{"VPMULLD Y1,Y2,Y3", "VPMULLD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d40d9"},
|
{"VPMULLD Y1,Y2,Y3", "VPMULLD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d40d9", ""},
|
||||||
{"VPUNPCKLDQ Y4,Y3,Y5", "VPUNPCKLDQ", []Operand{vreg(t, "Y4"), vreg(t, "Y3"), vreg(t, "Y5")}, "c5e562ec"},
|
{"VPUNPCKLDQ Y4,Y3,Y5", "VPUNPCKLDQ", []Operand{vreg(t, "Y4"), vreg(t, "Y3"), vreg(t, "Y5")}, "c5e562ec", ""},
|
||||||
{"VPERMD Y1,Y2,Y3", "VPERMD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d36d9"},
|
{"VPERMD Y1,Y2,Y3", "VPERMD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d36d9", ""},
|
||||||
// Floating point (packed and scalar) and FMA — same NDS form, the pp
|
// Floating point (packed and scalar) and FMA — same NDS form, the pp
|
||||||
// bits and map select the operation.
|
// bits and map select the operation.
|
||||||
{"VADDPD Y9,Y8,Y8", "VADDPD", []Operand{vreg(t, "Y9"), vreg(t, "Y8"), vreg(t, "Y8")}, "c4413d58c1"},
|
{"VADDPD Y9,Y8,Y8", "VADDPD", []Operand{vreg(t, "Y9"), vreg(t, "Y8"), vreg(t, "Y8")}, "c4413d58c1", ""},
|
||||||
{"VADDPD X1,X2,X3", "VADDPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e958d9"},
|
{"VADDPD X1,X2,X3", "VADDPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e958d9", ""},
|
||||||
{"VMULPD Y12,Y12,Y12", "VMULPD", []Operand{vreg(t, "Y12"), vreg(t, "Y12"), vreg(t, "Y12")}, "c4411d59e4"},
|
{"VMULPD Y12,Y12,Y12", "VMULPD", []Operand{vreg(t, "Y12"), vreg(t, "Y12"), vreg(t, "Y12")}, "c4411d59e4", ""},
|
||||||
{"VXORPD Y8,Y8,Y8", "VXORPD", []Operand{vreg(t, "Y8"), vreg(t, "Y8"), vreg(t, "Y8")}, "c4413d57c0"},
|
{"VXORPD Y8,Y8,Y8", "VXORPD", []Operand{vreg(t, "Y8"), vreg(t, "Y8"), vreg(t, "Y8")}, "c4413d57c0", ""},
|
||||||
{"VUNPCKHPD X8,X8,X9", "VUNPCKHPD", []Operand{vreg(t, "X8"), vreg(t, "X8"), vreg(t, "X9")}, "c4413915c8"},
|
{"VUNPCKHPD X8,X8,X9", "VUNPCKHPD", []Operand{vreg(t, "X8"), vreg(t, "X8"), vreg(t, "X9")}, "c4413915c8", ""},
|
||||||
{"VADDSD X9,X8,X8", "VADDSD", []Operand{vreg(t, "X9"), vreg(t, "X8"), vreg(t, "X8")}, "c4413b58c1"},
|
{"VADDSD X9,X8,X8", "VADDSD", []Operand{vreg(t, "X9"), vreg(t, "X8"), vreg(t, "X8")}, "c4413b58c1", ""},
|
||||||
{"VMULSD X0,X1,X1", "VMULSD", []Operand{vreg(t, "X0"), vreg(t, "X1"), vreg(t, "X1")}, "c5f359c8"},
|
{"VMULSD X0,X1,X1", "VMULSD", []Operand{vreg(t, "X0"), vreg(t, "X1"), vreg(t, "X1")}, "c5f359c8", ""},
|
||||||
{"VFMADD231PD Y14,Y12,Y8", "VFMADD231PD", []Operand{vreg(t, "Y14"), vreg(t, "Y12"), vreg(t, "Y8")}, "c4429db8c6"},
|
{"VFMADD231PD Y14,Y12,Y8", "VFMADD231PD", []Operand{vreg(t, "Y14"), vreg(t, "Y12"), vreg(t, "Y8")}, "c4429db8c6", ""},
|
||||||
{"VFMADD231PD (DI),Y12,Y8", "VFMADD231PD", []Operand{Ptr(DI, 0, 32), vreg(t, "Y12"), vreg(t, "Y8")}, "c4629db807"},
|
{"VFMADD231PD (DI),Y12,Y8", "VFMADD231PD", []Operand{Ptr(DI, 0, 32), vreg(t, "Y12"), vreg(t, "Y8")}, "c4629db807", ""},
|
||||||
// Two-operand reg/rm form (v̄vvv must be 1111).
|
// Two-operand reg/rm form (v̄vvv must be 1111).
|
||||||
{"VPMOVSXDQ X0,Y4", "VPMOVSXDQ", []Operand{vreg(t, "X0"), vreg(t, "Y4")}, "c4e27d25e0"},
|
{"VPMOVSXDQ X0,Y4", "VPMOVSXDQ", []Operand{vreg(t, "X0"), vreg(t, "Y4")}, "c4e27d25e0", ""},
|
||||||
{"VPMOVSXWD (SI),Y0", "VPMOVSXWD", []Operand{Ptr(SI, 0, 8), vreg(t, "Y0")}, "c4e27d2306"},
|
{"VPMOVSXWD (SI),Y0", "VPMOVSXWD", []Operand{Ptr(SI, 0, 8), vreg(t, "Y0")}, "c4e27d2306", ""},
|
||||||
{"VPBROADCASTD X0,Y15", "VPBROADCASTD", []Operand{vreg(t, "X0"), vreg(t, "Y15")}, "c4627d58f8"},
|
{"VPBROADCASTD X0,Y15", "VPBROADCASTD", []Operand{vreg(t, "X0"), vreg(t, "Y15")}, "c4627d58f8", ""},
|
||||||
{"VCVTDQ2PD X12,Y12", "VCVTDQ2PD", []Operand{vreg(t, "X12"), vreg(t, "Y12")}, "c4417ee6e4"},
|
{"VCVTDQ2PD X12,Y12", "VCVTDQ2PD", []Operand{vreg(t, "X12"), vreg(t, "Y12")}, "c4417ee6e4", ""},
|
||||||
{"VCVTDQ2PD (SI),Y4", "VCVTDQ2PD", []Operand{Ptr(SI, 0, 16), vreg(t, "Y4")}, "c5fee626"},
|
{"VCVTDQ2PD (SI),Y4", "VCVTDQ2PD", []Operand{Ptr(SI, 0, 16), vreg(t, "Y4")}, "c5fee626", ""},
|
||||||
{"VPMOVMSKB X11,AX", "VPMOVMSKB", []Operand{vreg(t, "X11"), AX}, "c4c179d7c3"},
|
{"VPMOVMSKB X11,AX", "VPMOVMSKB", []Operand{vreg(t, "X11"), AX}, "c4c179d7c3", ""},
|
||||||
{"VMOVMSKPS Y7,AX", "VMOVMSKPS", []Operand{vreg(t, "Y7"), AX}, "c5fc50c7"},
|
{"VMOVMSKPS Y7,AX", "VMOVMSKPS", []Operand{vreg(t, "Y7"), AX}, "c5fc50c7", ""},
|
||||||
// Immediate shifts.
|
// Immediate shifts.
|
||||||
{"VPSLLD $1,Y3,Y4", "VPSLLD", []Operand{Imm(1), vreg(t, "Y3"), vreg(t, "Y4")}, "c5dd72f301"},
|
{"VPSLLD $1,Y3,Y4", "VPSLLD", []Operand{Imm(1), vreg(t, "Y3"), vreg(t, "Y4")}, "c5dd72f301", ""},
|
||||||
{"VPSRLQ $2,Y5,Y6", "VPSRLQ", []Operand{Imm(2), vreg(t, "Y5"), vreg(t, "Y6")}, "c5cd73d502"},
|
{"VPSRLQ $2,Y5,Y6", "VPSRLQ", []Operand{Imm(2), vreg(t, "Y5"), vreg(t, "Y6")}, "c5cd73d502", ""},
|
||||||
// Variable-count shifts: the count lives in an XMM register or memory
|
// Variable-count shifts: the count lives in an XMM register or memory
|
||||||
// and the instruction takes the NDS form.
|
// and the instruction takes the NDS form.
|
||||||
{"VPSRLQ X0,Y8,Y8", "VPSRLQ", []Operand{vreg(t, "X0"), vreg(t, "Y8"), vreg(t, "Y8")}, "c53dd3c0"},
|
{"VPSRLQ X0,Y8,Y8", "VPSRLQ", []Operand{vreg(t, "X0"), vreg(t, "Y8"), vreg(t, "Y8")}, "c53dd3c0", ""},
|
||||||
{"VPSRLQ (AX),Y8,Y8", "VPSRLQ", []Operand{Ptr(AX, 0, 16), vreg(t, "Y8"), vreg(t, "Y8")}, "c53dd300"},
|
{"VPSRLQ (AX),Y8,Y8", "VPSRLQ", []Operand{Ptr(AX, 0, 16), vreg(t, "Y8"), vreg(t, "Y8")}, "c53dd300", ""},
|
||||||
{"VPSLLD X0,Y1,Y2", "VPSLLD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5f2d0"},
|
{"VPSLLD X0,Y1,Y2", "VPSLLD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5f2d0", ""},
|
||||||
{"VPSRLD X0,Y1,Y2", "VPSRLD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5d2d0"},
|
{"VPSRLD X0,Y1,Y2", "VPSRLD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5d2d0", ""},
|
||||||
{"VPSRAD X0,Y1,Y2", "VPSRAD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5e2d0"},
|
{"VPSRAD X0,Y1,Y2", "VPSRAD", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5e2d0", ""},
|
||||||
{"VPSLLQ X0,Y1,Y2", "VPSLLQ", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5f3d0"},
|
{"VPSLLQ X0,Y1,Y2", "VPSLLQ", []Operand{vreg(t, "X0"), vreg(t, "Y1"), vreg(t, "Y2")}, "c5f5f3d0", ""},
|
||||||
// Immediate shuffle (reg=dst, rm=src, imm8).
|
// Immediate shuffle (reg=dst, rm=src, imm8).
|
||||||
{"VPSHUFD $0xEE,X8,X9", "VPSHUFD", []Operand{Imm(0xEE), vreg(t, "X8"), vreg(t, "X9")}, "c4417970c8ee"},
|
{"VPSHUFD $0xEE,X8,X9", "VPSHUFD", []Operand{Imm(0xEE), vreg(t, "X8"), vreg(t, "X9")}, "c4417970c8ee", ""},
|
||||||
{"VPSHUFD $0xEE,Y1,Y2", "VPSHUFD", []Operand{Imm(0xEE), vreg(t, "Y1"), vreg(t, "Y2")}, "c5fd70d1ee"},
|
{"VPSHUFD $0xEE,Y1,Y2", "VPSHUFD", []Operand{Imm(0xEE), vreg(t, "Y1"), vreg(t, "Y2")}, "c5fd70d1ee", ""},
|
||||||
{"VPERMQ $0x1B,Y1,Y2", "VPERMQ", []Operand{Imm(0x1B), vreg(t, "Y1"), vreg(t, "Y2")}, "c4e3fd00d11b"},
|
{"VPERMQ $0x1B,Y1,Y2", "VPERMQ", []Operand{Imm(0x1B), vreg(t, "Y1"), vreg(t, "Y2")}, "c4e3fd00d11b", ""},
|
||||||
{"VPERMQ $0x1B,Y11,Y12", "VPERMQ", []Operand{Imm(0x1B), vreg(t, "Y11"), vreg(t, "Y12")}, "c443fd00e31b"},
|
{"VPERMQ $0x1B,Y11,Y12", "VPERMQ", []Operand{Imm(0x1B), vreg(t, "Y11"), vreg(t, "Y12")}, "c443fd00e31b", ""},
|
||||||
// Three-operand + immediate (reg=dst, vvvv=src1, rm=src2, imm8).
|
// Three-operand + immediate (reg=dst, vvvv=src1, rm=src2, imm8).
|
||||||
{"VSHUFPD $1,X1,X2,X3", "VSHUFPD", []Operand{Imm(1), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e9c6d901"},
|
{"VSHUFPD $1,X1,X2,X3", "VSHUFPD", []Operand{Imm(1), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e9c6d901", ""},
|
||||||
{"VSHUFPD $1,Y1,Y2,Y3", "VSHUFPD", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5edc6d901"},
|
{"VSHUFPD $1,Y1,Y2,Y3", "VSHUFPD", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5edc6d901", ""},
|
||||||
{"VPERM2I128 $0x31,Y1,Y2,Y3", "VPERM2I128", []Operand{Imm(0x31), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e36d46d931"},
|
{"VPERM2I128 $0x31,Y1,Y2,Y3", "VPERM2I128", []Operand{Imm(0x31), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e36d46d931", ""},
|
||||||
{"VINSERTI128 $1,X5,Y1,Y2", "VINSERTI128", []Operand{Imm(1), vreg(t, "X5"), vreg(t, "Y1"), vreg(t, "Y2")}, "c4e37538d501"},
|
{"VINSERTI128 $1,X5,Y1,Y2", "VINSERTI128", []Operand{Imm(1), vreg(t, "X5"), vreg(t, "Y1"), vreg(t, "Y2")}, "c4e37538d501", ""},
|
||||||
// Lane extract (reg=YMM source, rm=XMM/memory destination, imm8).
|
// Lane extract (reg=YMM source, rm=XMM/memory destination, imm8).
|
||||||
{"VEXTRACTI128 $1,Y8,X9", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d39c101"},
|
{"VEXTRACTI128 $1,Y8,X9", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d39c101", ""},
|
||||||
{"VEXTRACTI128 $1,Y8,(DI)", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), Ptr(DI, 0, 16)}, "c4637d390701"},
|
{"VEXTRACTI128 $1,Y8,(DI)", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), Ptr(DI, 0, 16)}, "c4637d390701", ""},
|
||||||
{"VEXTRACTF128 $1,Y8,X9", "VEXTRACTF128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d19c101"},
|
{"VEXTRACTF128 $1,Y8,X9", "VEXTRACTF128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d19c101", ""},
|
||||||
// Moves — each direction picks its own opcode and VEX.W.
|
// Moves — each direction picks its own opcode and VEX.W.
|
||||||
{"VMOVDQU (SI),Y1", "VMOVDQU", []Operand{Ptr(SI, 0, 32), vreg(t, "Y1")}, "c5fe6f0e"},
|
{"VMOVDQU (SI),Y1", "VMOVDQU", []Operand{Ptr(SI, 0, 32), vreg(t, "Y1")}, "c5fe6f0e", ""},
|
||||||
{"VMOVDQU Y3,(DI)", "VMOVDQU", []Operand{vreg(t, "Y3"), Ptr(DI, 0, 32)}, "c5fe7f1f"},
|
{"VMOVDQU Y3,(DI)", "VMOVDQU", []Operand{vreg(t, "Y3"), Ptr(DI, 0, 32)}, "c5fe7f1f", ""},
|
||||||
{"VMOVDQU X1,X2", "VMOVDQU", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fa7fca"},
|
{"VMOVDQU X1,X2", "VMOVDQU", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fa7fca", ""},
|
||||||
{"VMOVUPD (DI),Y14", "VMOVUPD", []Operand{Ptr(DI, 0, 32), vreg(t, "Y14")}, "c57d1037"},
|
{"VMOVUPD (DI),Y14", "VMOVUPD", []Operand{Ptr(DI, 0, 32), vreg(t, "Y14")}, "c57d1037", ""},
|
||||||
{"VMOVUPD Y14,(DI)", "VMOVUPD", []Operand{vreg(t, "Y14"), Ptr(DI, 0, 32)}, "c57d1137"},
|
{"VMOVUPD Y14,(DI)", "VMOVUPD", []Operand{vreg(t, "Y14"), Ptr(DI, 0, 32)}, "c57d1137", ""},
|
||||||
{"VMOVUPD X1,X2", "VMOVUPD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f911ca"},
|
{"VMOVUPD X1,X2", "VMOVUPD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f911ca", ""},
|
||||||
{"VMOVQ X8,AX", "VMOVQ", []Operand{vreg(t, "X8"), AX}, "c461f97ec0"},
|
{"VMOVQ X8,AX", "VMOVQ", []Operand{vreg(t, "X8"), AX}, "c461f97ec0", ""},
|
||||||
{"VMOVQ AX,X9", "VMOVQ", []Operand{AX, vreg(t, "X9")}, "c461f96ec8"},
|
{"VMOVQ AX,X9", "VMOVQ", []Operand{AX, vreg(t, "X9")}, "c461f96ec8", ""},
|
||||||
{"VMOVQ X8,(DI)", "VMOVQ", []Operand{vreg(t, "X8"), Ptr(DI, 0, 8)}, "c461f97e07"},
|
{"VMOVQ X8,(DI)", "VMOVQ", []Operand{vreg(t, "X8"), Ptr(DI, 0, 8)}, "c461f97e07", ""},
|
||||||
{"VMOVQ (SI),X9", "VMOVQ", []Operand{Ptr(SI, 0, 8), vreg(t, "X9")}, "c461f96e0e"},
|
{"VMOVQ (SI),X9", "VMOVQ", []Operand{Ptr(SI, 0, 8), vreg(t, "X9")}, "c461f96e0e", ""},
|
||||||
{"VMOVQ X8,X2", "VMOVQ", []Operand{vreg(t, "X8"), vreg(t, "X2")}, "c579d6c2"},
|
{"VMOVQ X8,X2", "VMOVQ", []Operand{vreg(t, "X8"), vreg(t, "X2")}, "c579d6c2", ""},
|
||||||
{"VMOVQ X2,X8", "VMOVQ", []Operand{vreg(t, "X2"), vreg(t, "X8")}, "c4c179d6d0"},
|
{"VMOVQ X2,X8", "VMOVQ", []Operand{vreg(t, "X2"), vreg(t, "X8")}, "c4c179d6d0", ""},
|
||||||
{"VMOVD X0,(SI)", "VMOVD", []Operand{vreg(t, "X0"), Ptr(SI, 0, 4)}, "c5f97e06"},
|
{"VMOVD X0,(SI)", "VMOVD", []Operand{vreg(t, "X0"), Ptr(SI, 0, 4)}, "c5f97e06", ""},
|
||||||
{"VMOVD AX,X0", "VMOVD", []Operand{AX, vreg(t, "X0")}, "c5f96ec0"},
|
{"VMOVD AX,X0", "VMOVD", []Operand{AX, vreg(t, "X0")}, "c5f96ec0", ""},
|
||||||
{"VMOVSD (SI),X8", "VMOVSD", []Operand{Ptr(SI, 0, 8), vreg(t, "X8")}, "c57b1006"},
|
{"VMOVSD (SI),X8", "VMOVSD", []Operand{Ptr(SI, 0, 8), vreg(t, "X8")}, "c57b1006", ""},
|
||||||
{"VMOVSD X8,(SI)", "VMOVSD", []Operand{vreg(t, "X8"), Ptr(SI, 0, 8)}, "c57b1106"},
|
{"VMOVSD X8,(SI)", "VMOVSD", []Operand{vreg(t, "X8"), Ptr(SI, 0, 8)}, "c57b1106", ""},
|
||||||
|
// Packed double arithmetic and unpack — the NDS form, the opcode
|
||||||
|
// selects the operation.
|
||||||
|
{"VSUBPD Y1,Y2,Y3", "VSUBPD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5ed5cd9", ""},
|
||||||
|
{"VDIVPD X1,X2,X3", "VDIVPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e95ed9", ""},
|
||||||
|
{"VMINPD Y1,Y2,Y3", "VMINPD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5ed5dd9", ""},
|
||||||
|
{"VMAXPD X4,X5,X6", "VMAXPD", []Operand{vreg(t, "X4"), vreg(t, "X5"), vreg(t, "X6")}, "c5d15ff4", ""},
|
||||||
|
{"VUNPCKLPD X1,X2,X3", "VUNPCKLPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e914d9", ""},
|
||||||
|
{"VUNPCKLPD Y1,Y2,Y3", "VUNPCKLPD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5ed14d9", ""},
|
||||||
|
{"VSUBPD (AX),X1,X2", "VSUBPD", []Operand{Ptr(AX, 0, 16), vreg(t, "X1"), vreg(t, "X2")}, "c5f15c10", ""},
|
||||||
|
// Scalar double and single arithmetic (F2 / F3 pp, 128-bit only).
|
||||||
|
{"VSUBSD X1,X2,X3", "VSUBSD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5eb5cd9", ""},
|
||||||
|
{"VDIVSD X7,X1,X2", "VDIVSD", []Operand{vreg(t, "X7"), vreg(t, "X1"), vreg(t, "X2")}, "c5f35ed7", ""},
|
||||||
|
{"VMINSD X1,X2,X3", "VMINSD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5eb5dd9", ""},
|
||||||
|
{"VMAXSD X3,X4,X5", "VMAXSD", []Operand{vreg(t, "X3"), vreg(t, "X4"), vreg(t, "X5")}, "c5db5feb", ""},
|
||||||
|
{"VADDSS X1,X2,X3", "VADDSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5ea58d9", ""},
|
||||||
|
{"VSUBSS X1,X2,X3", "VSUBSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5ea5cd9", ""},
|
||||||
|
{"VMULSS X9,X10,X11", "VMULSS", []Operand{vreg(t, "X9"), vreg(t, "X10"), vreg(t, "X11")}, "c4412a59d9", ""},
|
||||||
|
{"VDIVSS X1,X2,X3", "VDIVSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5ea5ed9", ""},
|
||||||
|
{"VMINSS X6,X7,X8", "VMINSS", []Operand{vreg(t, "X6"), vreg(t, "X7"), vreg(t, "X8")}, "c5425dc6", ""},
|
||||||
|
{"VMAXSS X1,X2,X3", "VMAXSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5ea5fd9", ""},
|
||||||
|
{"VADDSD 8(AX),X1,X2", "VADDSD", []Operand{Ptr(AX, 8, 8), vreg(t, "X1"), vreg(t, "X2")}, "c5f3585008", ""},
|
||||||
|
// VMOVDDUP — duplicate the low double (reg=dst, rm=src, F2 pp).
|
||||||
|
{"VMOVDDUP X1,X2", "VMOVDDUP", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fb12d1", ""},
|
||||||
|
{"VMOVDDUP Y1,Y2", "VMOVDDUP", []Operand{vreg(t, "Y1"), vreg(t, "Y2")}, "c5ff12d1", ""},
|
||||||
|
{"VMOVDDUP 8(AX),X1", "VMOVDDUP", []Operand{Ptr(AX, 8, 8), vreg(t, "X1")}, "c5fb124808", ""},
|
||||||
|
// Conversions: DQ→PS (no prefix), PS→PD (Go emits it without the F3
|
||||||
|
// prefix — see the table comment), DQ→PD.
|
||||||
|
{"VCVTDQ2PS X1,X2", "VCVTDQ2PS", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f85bd1", ""},
|
||||||
|
{"VCVTDQ2PS Y3,Y4", "VCVTDQ2PS", []Operand{vreg(t, "Y3"), vreg(t, "Y4")}, "c5fc5be3", ""},
|
||||||
|
{"VCVTPS2PD X1,X2", "VCVTPS2PD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f85ad1", ""},
|
||||||
|
{"VCVTPS2PD X1,Y2", "VCVTPS2PD", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c5fc5ad1", ""},
|
||||||
|
// PD→DQ conversions: the X/Y spellings fix the source length and the
|
||||||
|
// destination is always XMM; the decoder reports the base mnemonic.
|
||||||
|
{"VCVTPD2DQX X1,X2", "VCVTPD2DQX", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fbe6d1", "VCVTPD2DQ"},
|
||||||
|
{"VCVTPD2DQY Y1,X2", "VCVTPD2DQY", []Operand{vreg(t, "Y1"), vreg(t, "X2")}, "c5ffe6d1", "VCVTPD2DQ"},
|
||||||
|
{"VCVTTPD2DQX X3,X4", "VCVTTPD2DQX", []Operand{vreg(t, "X3"), vreg(t, "X4")}, "c5f9e6e3", "VCVTTPD2DQ"},
|
||||||
|
{"VCVTTPD2DQY Y5,X6", "VCVTTPD2DQY", []Operand{vreg(t, "Y5"), vreg(t, "X6")}, "c5fde6f5", "VCVTTPD2DQ"},
|
||||||
|
{"VCVTPD2DQY (AX),X1", "VCVTPD2DQY", []Operand{Ptr(AX, 0, 32), vreg(t, "X1")}, "c5ffe608", "VCVTPD2DQ"},
|
||||||
// No-operand.
|
// No-operand.
|
||||||
{"VZEROUPPER", "VZEROUPPER", nil, "c5f877"},
|
{"VZEROUPPER", "VZEROUPPER", nil, "c5f877", ""},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
code, err := Encode(c.mnem, c.ops...)
|
code, err := Encode(c.mnem, c.ops...)
|
||||||
@@ -251,7 +290,11 @@ func TestVexGroundTruth(t *testing.T) {
|
|||||||
if inst.Len != len(code) {
|
if inst.Len != len(code) {
|
||||||
t.Errorf("%s: Decode consumed %d of %d bytes", c.name, inst.Len, len(code))
|
t.Errorf("%s: Decode consumed %d of %d bytes", c.name, inst.Len, len(code))
|
||||||
}
|
}
|
||||||
if inst.Op.String() != c.mnem {
|
wantOp := c.wantOp
|
||||||
|
if wantOp == "" {
|
||||||
|
wantOp = c.mnem
|
||||||
|
}
|
||||||
|
if inst.Op.String() != wantOp {
|
||||||
t.Errorf("%s: decoded as %s", c.name, inst.Op.String())
|
t.Errorf("%s: decoded as %s", c.name, inst.Op.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-28
@@ -28,7 +28,7 @@ import (
|
|||||||
|
|
||||||
// version is the release version, stamped at build time via
|
// version is the release version, stamped at build time via
|
||||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||||
var version = "0.7.0"
|
var version = "0.12.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -49,31 +49,73 @@ func main() {
|
|||||||
case "lsp":
|
case "lsp":
|
||||||
os.Exit(cmdLSP(os.Args[2:]))
|
os.Exit(cmdLSP(os.Args[2:]))
|
||||||
case "version", "--version", "-V":
|
case "version", "--version", "-V":
|
||||||
fmt.Printf("gasm %s\n", version)
|
os.Exit(cmdVersion())
|
||||||
case "help", "-h", "--help":
|
case "help", "--help", "-h":
|
||||||
usage(os.Stdout)
|
usage(os.Stdout)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "gasm: unknown command %q\n\n", os.Args[1])
|
fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1])
|
||||||
usage(os.Stderr)
|
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cmdVersion prints the release version.
|
||||||
|
func cmdVersion() int {
|
||||||
|
fmt.Printf("gasm %s\n", version)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler
|
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler (GAsm)
|
||||||
|
|
||||||
|
gasm bundles a lexer, parser, formatter, linter, standalone assembler and
|
||||||
|
language server for Plan 9 assembly into one self-contained binary.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
gasm tokens <file> print the lexical token stream
|
gasm <command> [arguments]
|
||||||
gasm parse <file> parse and report syntax errors
|
gasm [flags]
|
||||||
gasm fmt [-w] [path...] canonicalise formatting (no path or a directory:
|
|
||||||
reformat every .s below it in place, like go fmt)
|
Commands:
|
||||||
gasm lint <file...> run static checks
|
tokens print the lexical token stream
|
||||||
gasm asm [-o out.bin] <file> assemble to machine code (amd64, Phase 2)
|
parse parse and report syntax errors
|
||||||
gasm lsp run the language server over stdio
|
fmt canonicalise formatting (gofmt for assembly)
|
||||||
gasm version print the version
|
lint run static checks
|
||||||
|
asm assemble .s files to machine code (amd64)
|
||||||
|
lsp run the language server over stdio
|
||||||
|
version print the version (same as --version)
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help show this help
|
||||||
|
-V, --version print the version
|
||||||
|
|
||||||
|
Run "gasm <command> -h" for a command's usage and flags.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gasm fmt reformat every .s below the current directory
|
||||||
|
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)
|
`, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newCommand returns the FlagSet of a subcommand whose -h/--help prints a
|
||||||
|
// proper usage block: the one-line usage, the long description and the flag
|
||||||
|
// defaults. The flag package routes -h/--help to fs.Usage and exits 0.
|
||||||
|
func newCommand(name, usageLine, long string) *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet(name, flag.ExitOnError)
|
||||||
|
fs.Usage = func() {
|
||||||
|
w := fs.Output()
|
||||||
|
fmt.Fprintf(w, "Usage: %s\n\n%s\n", usageLine, strings.TrimSpace(long))
|
||||||
|
hasFlags := false
|
||||||
|
fs.VisitAll(func(*flag.Flag) { hasFlags = true })
|
||||||
|
if hasFlags {
|
||||||
|
fmt.Fprintln(w, "\nFlags:")
|
||||||
|
fs.PrintDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
// readSource returns the contents of path, or stdin when path is "-".
|
// readSource returns the contents of path, or stdin when path is "-".
|
||||||
func readSource(path string) (string, error) {
|
func readSource(path string) (string, error) {
|
||||||
if path == "-" {
|
if path == "-" {
|
||||||
@@ -85,7 +127,10 @@ func readSource(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdTokens(args []string) int {
|
func cmdTokens(args []string) int {
|
||||||
fs := flag.NewFlagSet("tokens", flag.ExitOnError)
|
fs := newCommand("tokens", "gasm tokens <file>", `
|
||||||
|
Print the lexical token stream of FILE: position, token kind and text, one
|
||||||
|
token per line. FILE may be "-" to read standard input.
|
||||||
|
`)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>")
|
fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>")
|
||||||
@@ -103,7 +148,11 @@ func cmdTokens(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdParse(args []string) int {
|
func cmdParse(args []string) int {
|
||||||
fs := flag.NewFlagSet("parse", flag.ExitOnError)
|
fs := newCommand("parse", "gasm parse <file>", `
|
||||||
|
Parse FILE and report syntax errors on stderr. On success, print how many
|
||||||
|
declarations and TEXT functions the file contains. FILE may be "-" to read
|
||||||
|
standard input.
|
||||||
|
`)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm parse <file>")
|
fmt.Fprintln(os.Stderr, "usage: gasm parse <file>")
|
||||||
@@ -133,14 +182,24 @@ func cmdParse(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdFmt(args []string) int {
|
func cmdFmt(args []string) int {
|
||||||
fs_ := flag.NewFlagSet("fmt", flag.ExitOnError)
|
fs := newCommand("fmt", "gasm fmt [-w] [path...]", `
|
||||||
write := fs_.Bool("w", false, "write result to the source file")
|
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
|
||||||
fs_.Parse(args)
|
spacing, per-function mnemonic alignment and blank-line layout (exactly one
|
||||||
|
blank line before each label, TEXT and GLOBL block). Formatting is
|
||||||
|
idempotent and preserves every line, comments included.
|
||||||
|
|
||||||
|
With no paths — or a directory path — every .s file below it is reformatted
|
||||||
|
in place and the changed files are listed, the way go fmt does; "." and "_"
|
||||||
|
directories are skipped. Explicit file paths print to stdout unless -w is
|
||||||
|
given.
|
||||||
|
`)
|
||||||
|
write := fs.Bool("w", false, "write result to the source file")
|
||||||
|
fs.Parse(args)
|
||||||
// Like go fmt: with no arguments, or with a directory argument, every .s
|
// Like go fmt: with no arguments, or with a directory argument, every .s
|
||||||
// file below the directory is formatted in place and the names of the
|
// file below the directory is formatted in place and the names of the
|
||||||
// changed files are listed; explicit file arguments keep the -w / stdout
|
// changed files are listed; explicit file arguments keep the -w / stdout
|
||||||
// behaviour.
|
// behaviour.
|
||||||
paths := fs_.Args()
|
paths := fs.Args()
|
||||||
dirMode := len(paths) == 0
|
dirMode := len(paths) == 0
|
||||||
if dirMode {
|
if dirMode {
|
||||||
paths = []string{"."}
|
paths = []string{"."}
|
||||||
@@ -215,7 +274,16 @@ func asmFiles(dir string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdLint(args []string) int {
|
func cmdLint(args []string) int {
|
||||||
fs := flag.NewFlagSet("lint", flag.ExitOnError)
|
fs := newCommand("lint", "gasm lint <file...>", `
|
||||||
|
Run the static checks over the given files and print diagnostics as
|
||||||
|
"file:line:col: severity: message [code]". The exit status is non-zero when
|
||||||
|
an error-severity diagnostic is found; warnings (e.g. the register-clobber
|
||||||
|
audit) do not affect it.
|
||||||
|
|
||||||
|
Rules include unknown-instruction, operand-count, undefined-label,
|
||||||
|
duplicate-label, missing-ret, missing-textflag-include, abi-argsize,
|
||||||
|
unreachable-code, register-clobber and funcdata-pcdata.
|
||||||
|
`)
|
||||||
disable := fs.String("disable", "", "comma-separated rule codes to disable")
|
disable := fs.String("disable", "", "comma-separated rule codes to disable")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() == 0 {
|
if fs.NArg() == 0 {
|
||||||
@@ -256,7 +324,13 @@ func cmdLint(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdLSP(args []string) int {
|
func cmdLSP(args []string) int {
|
||||||
fs := flag.NewFlagSet("lsp", flag.ExitOnError)
|
fs := newCommand("lsp", "gasm lsp", `
|
||||||
|
Run the language server over standard input/output: JSON-RPC 2.0 with
|
||||||
|
Content-Length framing. Point an LSP-capable editor at the binary and
|
||||||
|
associate it with .s files; the target architecture is inferred from the file
|
||||||
|
suffix (_amd64.s, _arm64.s, _riscv64.s, _loong64.s). Provides completion,
|
||||||
|
hover, document symbols, diagnostics and semantic-token highlighting.
|
||||||
|
`)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
srv := lsp.New(os.Stdin, os.Stdout)
|
srv := lsp.New(os.Stdin, os.Stdout)
|
||||||
if err := srv.Run(); err != nil {
|
if err := srv.Run(); err != nil {
|
||||||
@@ -267,11 +341,26 @@ func cmdLSP(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdAsm(args []string) int {
|
func cmdAsm(args []string) int {
|
||||||
fs := flag.NewFlagSet("asm", flag.ExitOnError)
|
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", `
|
||||||
out := fs.String("o", "", "write the concatenated machine code to this 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)
|
||||||
|
resolved RIP-relative — and printed as a hex dump.
|
||||||
|
|
||||||
|
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; 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, macho or goobj (Go object)")
|
||||||
|
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm asm [-o out.bin] <file>")
|
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
@@ -342,12 +431,38 @@ func cmdAsm(args []string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *out != "" {
|
if *out != "" {
|
||||||
all := img.Bytes()
|
var obj []byte
|
||||||
if err := os.WriteFile(*out, all, 0o644); err != nil {
|
var err error
|
||||||
|
var kind string
|
||||||
|
switch *format {
|
||||||
|
case "raw":
|
||||||
|
if len(img.Externals) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm asm: external symbol %q needs an object file (use --format elf or --format macho)\n", img.Externals[0])
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
obj, kind = img.Bytes(), "raw image"
|
||||||
|
case "elf":
|
||||||
|
obj, err = img.ELFObject()
|
||||||
|
kind = "ELF object"
|
||||||
|
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, macho or goobj)\n", *format)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "gasm asm:", err)
|
fmt.Fprintln(os.Stderr, "gasm asm:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
fmt.Printf("wrote %d bytes to %s\n", len(all), *out)
|
if err := os.WriteFile(*out, obj, 0o644); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm asm:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Printf("wrote %d bytes to %s (%s)\n", len(obj), *out, kind)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-2
@@ -200,8 +200,24 @@ func TestCmdFmtWrite(t *testing.T) {
|
|||||||
func TestUsage(t *testing.T) {
|
func TestUsage(t *testing.T) {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
usage(&b)
|
usage(&b)
|
||||||
if !strings.Contains(b.String(), "gasm") {
|
out := b.String()
|
||||||
t.Errorf("usage text unexpected:\n%s", b.String())
|
for _, want := range []string{
|
||||||
|
"gasm", "Commands:", "Flags:", "--help", "--version",
|
||||||
|
"tokens", "parse", "fmt", "lint", "asm", "lsp", "version",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Errorf("usage text missing %q:\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdVersion(t *testing.T) {
|
||||||
|
out, _, code := capture(func() int { return cmdVersion() })
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, version) {
|
||||||
|
t.Errorf("version output %q does not mention %q", out, version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-11
@@ -142,7 +142,7 @@ Two deeper analyses sit on top of the AST:
|
|||||||
has no System V style callee-saved registers (amd64 `BX`, `R12`–`R15` and
|
has no System V style callee-saved registers (amd64 `BX`, `R12`–`R15` and
|
||||||
the like are caller-saved or permanent scratch, and hand-written kernels may
|
the like are caller-saved or permanent scratch, and hand-written kernels may
|
||||||
clobber them freely). The audited set is the frame pointer and the
|
clobber them freely). The audited set is the frame pointer and the
|
||||||
goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/
|
the frame pointer, the goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/
|
||||||
`R29`, riscv64 `X27`, loong64 `R22`); the goroutine pointer is reported only
|
`R29`, riscv64 `X27`, loong64 `R22`); the goroutine pointer is reported only
|
||||||
when the function can reach the runtime — it is not `NOSPLIT` or makes a
|
when the function can reach the runtime — it is not `NOSPLIT` or makes a
|
||||||
call — since the ABI0 transition machinery restores it on those paths, and
|
call — since the ABI0 transition machinery restores it on those paths, and
|
||||||
@@ -213,15 +213,26 @@ three-operand-plus-immediate form (`VSHUFPD`,
|
|||||||
`VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`,
|
`VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`,
|
||||||
`VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or
|
`VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or
|
||||||
memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
|
memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
|
||||||
`VMOVD`, `VMOVQ`, `VMOVSD`), the floating-point and FMA arithmetic (`VADDPD`,
|
`VMOVD`, `VMOVQ`, `VMOVSD`), the floating-point and FMA arithmetic — the
|
||||||
`VMULPD`, `VXORPD`, `VUNPCKHPD`, the scalar `VADDSD`/`VMULSD`, `VCVTDQ2PD`,
|
packed double operations (`VADDPD`/`VSUBPD`/`VMULPD`/`VDIVPD`/`VMINPD`/
|
||||||
`VFMADD231PD`) and the no-operand `VZEROUPPER` — together with `VPERMD` and
|
`VMAXPD`), the unpacks (`VUNPCKHPD`/`VUNPCKLPD`), the scalar SD and SS
|
||||||
|
operations, `VMOVDDUP`, `VXORPD`, the width-changing conversions
|
||||||
|
(`VCVTDQ2PS`, `VCVTPS2PD`, `VCVTDQ2PD`, and the `VCVTPD2DQX`/`Y` and
|
||||||
|
`VCVTTPD2DQX`/`Y` spellings, whose length follows the wider source) and
|
||||||
|
`VFMADD231PD` — and the no-operand `VZEROUPPER`, together with `VPERMD` and
|
||||||
the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves,
|
the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves,
|
||||||
`CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix — the four-byte prefix with
|
`CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix — the four-byte prefix with
|
||||||
5-bit register fields (Z0–Z31, X/Y 16–31), opmask registers as operands and
|
5-bit register fields (Z0–Z31, X/Y 16–31, with the mod=11 quirk that carries
|
||||||
mask destinations, and the compressed disp8×N displacement, whose multiplier
|
rm[4] in X̄), opmask registers (K0–K7 as operands, mask destinations and
|
||||||
follows the memory operand's size — covering every instruction the go-flac
|
explicit merging/zeroing masks — written the way Go writes them, as a K
|
||||||
AVX2 and AVX-512 kernels use. Every encoding is validated two ways: by
|
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
|
||||||
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against
|
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
|
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
|
whole functions: all 27 functions of both kernels assemble to exactly the Go
|
||||||
@@ -232,9 +243,26 @@ File-level assembly (`AssembleFile`) goes beyond single functions: it
|
|||||||
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
|
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
|
||||||
behind the code and resolves references to them (`mask<>(SB)`) to
|
behind the code and resolves references to them (`mask<>(SB)`) to
|
||||||
RIP-relative loads whose displacements point inside the resulting image, so
|
RIP-relative loads whose displacements point inside the resulting image, so
|
||||||
the bytes are self-consistent at any base address. External (non-file-local)
|
the bytes are self-consistent at any base address. References to symbols no
|
||||||
symbols are rejected: they need object-file emission, which — together with
|
`GLOBL` defines are kept as relocations on the function layout, and the
|
||||||
EVEX masking/zeroing and the other architectures — is the rest of Phase 2.
|
object-file emitters turn the whole image into a linkable object: the ELF
|
||||||
|
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. 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
|
## Extension points
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||||
|
|
||||||
version := "0.7.0"
|
version := "0.12.0"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|||||||
+23
-1
@@ -242,7 +242,7 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if archKnown && !cfg.Disable[CodeOperandCount] && !isMacroInvocation(mnem, macros) {
|
if archKnown && !cfg.Disable[CodeOperandCount] && !isMacroInvocation(mnem, macros) && !maskedEvex(mnem, st.Operands) {
|
||||||
if in, ok := tab.Lookup(mnem); ok && in.MinOps >= 0 {
|
if in, ok := tab.Lookup(mnem); ok && in.MinOps >= 0 {
|
||||||
n := len(st.Operands)
|
n := len(st.Operands)
|
||||||
if n < in.MinOps || n > in.MaxOps {
|
if n < in.MinOps || n > in.MaxOps {
|
||||||
@@ -438,6 +438,28 @@ func isMacroInvocation(mnem string, macros map[string]bool) bool {
|
|||||||
return strings.Contains(mnem, "_") || macros[mnem]
|
return strings.Contains(mnem, "_") || macros[mnem]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maskedEvex reports whether the instruction is a masked EVEX form: the
|
||||||
|
// mnemonic carries a .Z suffix, or the operand list contains an opmask
|
||||||
|
// register (K1–K7). Either way the operand count differs from the unmasked
|
||||||
|
// form, so count checks are skipped.
|
||||||
|
func maskedEvex(mnem string, ops []*ast.Operand) bool {
|
||||||
|
if strings.Contains(mnem, ".") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, op := range ops {
|
||||||
|
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Base == "" &&
|
||||||
|
op.Addr.Index == "" && op.Addr.Sym.Pseudo == "" && isMaskReg(op.Addr.Sym.Name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isMaskReg reports whether name is an opmask register K0–K7.
|
||||||
|
func isMaskReg(name string) bool {
|
||||||
|
return len(name) == 2 && name[0] == 'K' && name[1] >= '0' && name[1] <= '7'
|
||||||
|
}
|
||||||
|
|
||||||
// isConditionalDirective reports whether a preprocessor directive (the text
|
// isConditionalDirective reports whether a preprocessor directive (the text
|
||||||
// after '#') is a conditional-compilation directive whose branches the parser
|
// after '#') is a conditional-compilation directive whose branches the parser
|
||||||
// cannot resolve.
|
// cannot resolve.
|
||||||
|
|||||||
@@ -187,6 +187,26 @@ done:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestEvexMaskingRecognised checks that masked EVEX forms — the .Z suffix and
|
||||||
|
// an explicit K operand — are recognised and exempt from operand-count
|
||||||
|
// checks.
|
||||||
|
func TestEvexMaskingRecognised(t *testing.T) {
|
||||||
|
diags := lintSrc(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
VPADDD.Z Z1, Z2, K2, Z3
|
||||||
|
VPMINSD Z1, Z2, K5, Z3
|
||||||
|
VMOVDQU8 Z1, K3, (SI)
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
if codes(diags)[CodeUnknownInstr] != 0 {
|
||||||
|
t.Fatalf("masked EVEX must be recognised: %+v", diags)
|
||||||
|
}
|
||||||
|
if codes(diags)[CodeOperandCount] != 0 {
|
||||||
|
t.Fatalf("masked operand counts must not be flagged: %+v", diags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestArm64AddressingSuffix(t *testing.T) {
|
func TestArm64AddressingSuffix(t *testing.T) {
|
||||||
// .W (pre-index) and .P (post-index) suffixes must resolve to the base
|
// .W (pre-index) and .P (post-index) suffixes must resolve to the base
|
||||||
// instruction.
|
// instruction.
|
||||||
|
|||||||
@@ -375,6 +375,11 @@ func parseImmediate(g []token.Token) ast.Immediate {
|
|||||||
if v, ok := tryInt(text); ok {
|
if v, ok := tryInt(text); ok {
|
||||||
imm.Val = v
|
imm.Val = v
|
||||||
imm.HasVal = true
|
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 {
|
} else {
|
||||||
imm.Float = text
|
imm.Float = text
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user