Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56ecc39539 |
+73
-19
@@ -23,6 +23,31 @@ import (
|
||||
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
|
||||
// integer and shuffle/extract/permute/move set is in.
|
||||
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
code, _, labels, err := assemble(t, nil)
|
||||
return code, labels, err
|
||||
}
|
||||
|
||||
// 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
|
||||
// rejects SB operands outright (single-function assembly cannot resolve
|
||||
// them).
|
||||
type linkInfo struct {
|
||||
symbols map[string]bool
|
||||
}
|
||||
|
||||
// sbPatch is a function-relative static-symbol relocation: the disp32 field
|
||||
// at off must become the symbol's address minus after, where after is the
|
||||
// function-relative address just past the instruction.
|
||||
type sbPatch struct {
|
||||
off int
|
||||
after int
|
||||
name string
|
||||
addend int64
|
||||
}
|
||||
|
||||
// assemble encodes a TEXT body, returning the machine code, the static-symbol
|
||||
// patch sites (for the file-level layout to resolve) and the label table.
|
||||
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, error) {
|
||||
fi := computeFrame(t)
|
||||
chain := jumpChain(t)
|
||||
resolve := func(name string) string {
|
||||
@@ -44,9 +69,9 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
case *ast.Label:
|
||||
offsets[s.Name.Text] = pos
|
||||
case *ast.Instr:
|
||||
sz, err := instrSize(s, fi, long[i])
|
||||
sz, err := instrSize(s, fi, long[i], link)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
}
|
||||
sizes[i] = sz
|
||||
pcs[i] = pos
|
||||
@@ -85,23 +110,25 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
|
||||
// Pass 2: emit.
|
||||
out := append([]byte(nil), fi.prologue...)
|
||||
var patches []sbPatch
|
||||
pos := len(fi.prologue)
|
||||
for i, stmt := range t.Body {
|
||||
s, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, err := encodeInstr(s, pos, offsets, fi, long[i], resolve)
|
||||
code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
}
|
||||
if len(code) != sizes[i] {
|
||||
return nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||
return nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||
}
|
||||
patches = append(patches, ps...)
|
||||
out = append(out, code...)
|
||||
pos += len(code)
|
||||
}
|
||||
return out, offsets, nil
|
||||
return out, patches, offsets, nil
|
||||
}
|
||||
|
||||
// jumpChain precomputes jump-to-jump folding: a label whose first instruction
|
||||
@@ -214,12 +241,12 @@ func addSP(size int) []byte { // ADDQ $size, SP
|
||||
// instrSize returns the encoded length of an instruction (layout pass).
|
||||
// encodeInstr already includes the epilogue for a RET in a frame-pointer
|
||||
// function; jumps use their short or long form (never an epilogue).
|
||||
func instrSize(s *ast.Instr, fi frameInfo, long bool) (int, error) {
|
||||
func instrSize(s *ast.Instr, fi frameInfo, long bool, link *linkInfo) (int, error) {
|
||||
mnem := strings.ToUpper(s.Mnemonic.Text)
|
||||
if isJumpMnemonic(mnem) {
|
||||
return jumpSize(mnem, long), nil
|
||||
}
|
||||
code, err := encodeInstr(s, 0, nil, fi, false, nil)
|
||||
code, _, err := encodeInstr(s, 0, nil, fi, false, nil, link)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -254,7 +281,7 @@ func jumpSize(mnem string, long bool) int {
|
||||
// (relative to pc, the instruction's own offset). A RET in a frame-pointer
|
||||
// function is prefixed with the epilogue. resolve, when non-nil, redirects a
|
||||
// jump label through the jump-to-jump chain before the offset lookup.
|
||||
func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string) ([]byte, error) {
|
||||
func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string, link *linkInfo) ([]byte, []sbPatch, error) {
|
||||
mnem := strings.ToUpper(s.Mnemonic.Text)
|
||||
|
||||
var prefix []byte
|
||||
@@ -263,32 +290,48 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon
|
||||
}
|
||||
|
||||
var code []byte
|
||||
var ps []sbPatch
|
||||
var err error
|
||||
if isJumpMnemonic(mnem) {
|
||||
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
||||
} else {
|
||||
code, err = encodeNormal(s, fi)
|
||||
code, ps, err = encodeNormal(s, fi, link)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
return append(prefix, code...), nil
|
||||
// Anchor the patch fields at function-relative positions: off indexes the
|
||||
// disp32 field, after is the address just past the instruction.
|
||||
body := pc + len(prefix)
|
||||
for i := range ps {
|
||||
ps[i].off += body
|
||||
ps[i].after = body + len(code)
|
||||
}
|
||||
return append(prefix, code...), ps, nil
|
||||
}
|
||||
|
||||
func encodeNormal(s *ast.Instr, fi frameInfo) ([]byte, error) {
|
||||
func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch, error) {
|
||||
_, size := splitSize(strings.ToUpper(s.Mnemonic.Text))
|
||||
if size == 0 {
|
||||
size = 8
|
||||
}
|
||||
ops := make([]Operand, len(s.Operands))
|
||||
for i, op := range s.Operands {
|
||||
o, err := operandFromAST(op, size, fi)
|
||||
o, err := operandFromAST(op, size, fi, link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
ops[i] = o
|
||||
}
|
||||
return Encode(s.Mnemonic.Text, ops...)
|
||||
e := &enc{}
|
||||
if err := e.encode(s.Mnemonic.Text, ops); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ps := make([]sbPatch, len(e.patches))
|
||||
for i, p := range e.patches {
|
||||
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend}
|
||||
}
|
||||
return e.out, ps, nil
|
||||
}
|
||||
|
||||
// encodeJump encodes a JMP/CALL/Jcc with a relative offset resolved from the
|
||||
@@ -345,7 +388,7 @@ var spReg = Reg{idx: 4, size: 8}
|
||||
|
||||
// operandFromAST converts a parsed operand into an encoder Operand, applying
|
||||
// the frame translation to FP/SP pseudo-register operands.
|
||||
func operandFromAST(op *ast.Operand, size int, fi frameInfo) (Operand, error) {
|
||||
func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Operand, error) {
|
||||
switch op.Kind {
|
||||
case ast.OpImmediate:
|
||||
if op.Imm.HasVal {
|
||||
@@ -371,9 +414,20 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo) (Operand, error) {
|
||||
off := fi.spAdjust + a.Sym.Offset
|
||||
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
|
||||
}
|
||||
// SB (global symbol) needs a relocation — not yet supported.
|
||||
// SB (global symbol): a static (file-local, <>) symbol becomes a
|
||||
// RIP-relative reference resolved by the file-level layout; anything
|
||||
// external needs object-file emission.
|
||||
if a.Sym != nil && a.Sym.Pseudo == "SB" {
|
||||
return nil, fmt.Errorf("SB (global symbol) operands need relocation support (pending)")
|
||||
if !a.Sym.Static {
|
||||
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
|
||||
}
|
||||
if link == nil || link.symbols == nil {
|
||||
return nil, fmt.Errorf("static symbol %q needs file-level assembly (AssembleFile)", a.Sym.Name)
|
||||
}
|
||||
if !link.symbols[a.Sym.Name] {
|
||||
return nil, fmt.Errorf("undefined symbol %q", a.Sym.Name)
|
||||
}
|
||||
return sbMem{size: size, name: a.Sym.Name, addend: a.Sym.Offset}, nil
|
||||
}
|
||||
|
||||
// Memory with a real base register: (base), off(base), (base)(index*scale).
|
||||
|
||||
@@ -20,6 +20,15 @@ func Encode(mnemonic string, ops ...Operand) ([]byte, error) {
|
||||
|
||||
type enc struct {
|
||||
out []byte
|
||||
patches []encPatch // disp32 fields awaiting static-symbol resolution
|
||||
}
|
||||
|
||||
// encPatch marks a 4-byte displacement field in enc.out that must receive the
|
||||
// RIP-relative offset of a static symbol once the file layout is settled.
|
||||
type encPatch struct {
|
||||
off int
|
||||
name string
|
||||
addend int64
|
||||
}
|
||||
|
||||
func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
@@ -120,6 +129,14 @@ type instr struct {
|
||||
sib int // -1 if absent
|
||||
disp []byte
|
||||
imm []byte
|
||||
sb *sbRef // static-symbol displacement in disp, awaiting resolution
|
||||
}
|
||||
|
||||
// sbRef records that an instruction's displacement refers to a static symbol
|
||||
// rather than holding a literal value.
|
||||
type sbRef struct {
|
||||
name string
|
||||
addend int64
|
||||
}
|
||||
|
||||
func (e *enc) emit(i *instr) error {
|
||||
@@ -152,6 +169,9 @@ func (e *enc) emit(i *instr) error {
|
||||
if i.sib >= 0 {
|
||||
e.out = append(e.out, byte(i.sib))
|
||||
}
|
||||
if i.sb != nil {
|
||||
e.patches = append(e.patches, encPatch{off: len(e.out), name: i.sb.name, addend: i.sb.addend})
|
||||
}
|
||||
e.out = append(e.out, i.disp...)
|
||||
e.out = append(e.out, i.imm...)
|
||||
return nil
|
||||
@@ -199,6 +219,13 @@ func setRMReg(i *instr, regField int, rexR, regForced bool, rm Operand, opSize i
|
||||
return nil
|
||||
case Mem:
|
||||
return setMem(i, regField, r)
|
||||
case sbMem:
|
||||
// RIP-relative reference; the displacement is patched once the static
|
||||
// symbol's address is known.
|
||||
i.modrm = regField<<3 | 0x05 // mod=00, rm=101 → (RIP)+disp32
|
||||
i.disp = le32(0)
|
||||
i.sb = &sbRef{name: r.name, addend: r.addend}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid r/m operand %T", rm)
|
||||
}
|
||||
|
||||
+15
-3
@@ -78,6 +78,17 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
||||
}
|
||||
return e.emit(i)
|
||||
|
||||
case sbMem:
|
||||
if !dstIsReg {
|
||||
return fmt.Errorf("MOV: two memory operands")
|
||||
}
|
||||
// MOV r, r/m: reg=dst, rm=src(static symbol).
|
||||
i := newInstr(size, []byte{movRR(size)})
|
||||
if err := setRM(i, dstReg, src, size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
|
||||
case Imm:
|
||||
if dstIsReg {
|
||||
// MOV r, imm: 0xB0+reg (8-bit) / 0xB8+reg (16/32/64, imm64 for Q).
|
||||
@@ -280,12 +291,13 @@ func (e *enc) encodeLea(ops []Operand, size int) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("LEA: destination must be a register")
|
||||
}
|
||||
mem, ok := src.(Mem)
|
||||
if !ok {
|
||||
switch src.(type) {
|
||||
case Mem, sbMem:
|
||||
default:
|
||||
return fmt.Errorf("LEA: source must be a memory operand")
|
||||
}
|
||||
i := newInstr(size, []byte{0x8D})
|
||||
if err := setRM(i, dstReg, mem, size); err != nil {
|
||||
if err := setRM(i, dstReg, src, size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
)
|
||||
|
||||
// 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
|
||||
// references are encoded RIP-relative and resolved within the image, so the
|
||||
// bytes are self-consistent and executable at any base address.
|
||||
type Image struct {
|
||||
Code []byte // concatenated function bodies
|
||||
Data []byte // static data section
|
||||
Funcs []FuncLayout // function positions, in source order
|
||||
Symbols map[string]int // static symbol → byte offset within the image
|
||||
}
|
||||
|
||||
// FuncLayout describes one assembled function within an Image.
|
||||
type FuncLayout struct {
|
||||
Name string
|
||||
Offset int // start offset within the image (== offset within Code)
|
||||
Size int
|
||||
Labels map[string]int // local labels, function-relative
|
||||
}
|
||||
|
||||
// Bytes returns the whole image: code, then data.
|
||||
func (img *Image) Bytes() []byte {
|
||||
out := make([]byte, 0, len(img.Code)+len(img.Data))
|
||||
out = append(out, img.Code...)
|
||||
return append(out, img.Data...)
|
||||
}
|
||||
|
||||
// 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
|
||||
// static-symbol reference becomes a RIP-relative load whose displacement is
|
||||
// resolved against that layout. External (non-file-local) symbol references
|
||||
// are rejected: they need object-file emission.
|
||||
func AssembleFile(f *ast.File) (*Image, error) {
|
||||
syms, order, err := collectData(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
known := make(map[string]bool, len(syms))
|
||||
for name := range syms {
|
||||
known[name] = true
|
||||
}
|
||||
link := &linkInfo{symbols: known}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
type asmFunc struct {
|
||||
name string
|
||||
patches []sbPatch
|
||||
}
|
||||
var funcs []asmFunc
|
||||
for _, d := range f.Decls {
|
||||
t, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, patches, labels, err := assemble(t, link)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
img.Funcs = append(img.Funcs, FuncLayout{
|
||||
Name: t.Name.Name,
|
||||
Offset: len(img.Code),
|
||||
Size: len(code),
|
||||
Labels: labels,
|
||||
})
|
||||
img.Code = append(img.Code, code...)
|
||||
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
||||
}
|
||||
|
||||
// Lay out the data section behind the code, each symbol 16-aligned.
|
||||
dataStart := len(img.Code)
|
||||
for _, name := range order {
|
||||
if pos := dataStart + len(img.Data); pos != align16(pos) {
|
||||
img.Data = append(img.Data, make([]byte, align16(pos)-pos)...)
|
||||
}
|
||||
img.Symbols[name] = dataStart + len(img.Data)
|
||||
img.Data = append(img.Data, syms[name]...)
|
||||
}
|
||||
|
||||
// Resolve the RIP-relative displacements now that every address is known.
|
||||
for i, fn := range funcs {
|
||||
base := img.Funcs[i].Offset
|
||||
code := img.Code[base : base+img.Funcs[i].Size]
|
||||
for _, p := range fn.patches {
|
||||
rel := int64(img.Symbols[p.name]) + 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))
|
||||
}
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// collectData gathers the file's static symbols (GLOBL) and their initial
|
||||
// contents (DATA) into byte buffers, in declaration order.
|
||||
func collectData(f *ast.File) (map[string][]byte, []string, error) {
|
||||
syms := map[string][]byte{}
|
||||
var order []string
|
||||
for _, d := range f.Decls {
|
||||
switch dd := d.(type) {
|
||||
case *ast.Globl:
|
||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
name := dd.Name.Name
|
||||
if _, dup := syms[name]; dup {
|
||||
return nil, nil, fmt.Errorf("duplicate GLOBL %q", name)
|
||||
}
|
||||
size := 0
|
||||
if dd.Size != nil && dd.Size.Imm.HasVal {
|
||||
size = int(dd.Size.Imm.Val)
|
||||
}
|
||||
syms[name] = make([]byte, size)
|
||||
order = append(order, name)
|
||||
|
||||
case *ast.Data:
|
||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
buf, ok := syms[dd.Name.Name]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name)
|
||||
}
|
||||
if dd.Value == nil || !dd.Value.Imm.HasVal {
|
||||
return nil, nil, fmt.Errorf("DATA %q: value must be an integer immediate", dd.Name.Name)
|
||||
}
|
||||
w := dd.Width
|
||||
switch w {
|
||||
case 1, 2, 4, 8:
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w)
|
||||
}
|
||||
off := dd.Name.Offset
|
||||
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))
|
||||
}
|
||||
v := dd.Value.Imm.Val
|
||||
if dd.Value.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
for i := 0; i < w; i++ {
|
||||
buf[off+int64(i)] = byte(v >> (8 * i))
|
||||
}
|
||||
}
|
||||
}
|
||||
return syms, order, nil
|
||||
}
|
||||
|
||||
// align16 rounds n up to the next multiple of 16.
|
||||
func align16(n int) int {
|
||||
return (n + 15) &^ 15
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestAssembleFileStaticData checks the whole-image layout — code, padding
|
||||
// and the data section — and that the RIP-relative displacements of static
|
||||
// symbol loads resolve to the right bytes.
|
||||
func TestAssembleFileStaticData(t *testing.T) {
|
||||
f, errs := parser.Parse("d_amd64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·load(SB), NOSPLIT, $0
|
||||
VMOVDQU mask<>(SB), X15
|
||||
MOVL small<>(SB), AX
|
||||
RET
|
||||
GLOBL mask<>(SB), RODATA, $16
|
||||
DATA mask<>+0(SB)/4, $0x80020100
|
||||
DATA mask<>+4(SB)/4, $0x80050403
|
||||
DATA mask<>+8(SB)/4, $0x80080706
|
||||
DATA mask<>+12(SB)/4, $0x800B0A09
|
||||
GLOBL small<>(SB), RODATA, $4
|
||||
DATA small<>+0(SB)/4, $0x1234
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
|
||||
// Code (15 bytes) + 1 pad byte to align the data section to 16:
|
||||
// VMOVDQU mask<>(SB), X15 c5 7a 6f 3d 08 00 00 00 (disp = 16 − 8)
|
||||
// MOVL small<>(SB), AX 8b 05 12 00 00 00 (disp = 32 − 14)
|
||||
// RET c3
|
||||
// Data: pad, mask (16 bytes), small (4 bytes).
|
||||
want := "c57a6f3d080000008b0512000000c300" +
|
||||
"000102800304058006070880090a0b80" +
|
||||
"34120000"
|
||||
if got := strings.ReplaceAll(hexBytes(img.Bytes()), " ", ""); got != want {
|
||||
t.Errorf("image bytes:\n got %s\n want %s", got, want)
|
||||
}
|
||||
if img.Symbols["mask"] != 16 || img.Symbols["small"] != 32 {
|
||||
t.Errorf("symbol offsets = %v, want mask=16 small=32", img.Symbols)
|
||||
}
|
||||
if len(img.Funcs) != 1 || img.Funcs[0].Name != "load" || img.Funcs[0].Size != 15 {
|
||||
t.Errorf("funcs = %+v", img.Funcs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFileErrors checks the static-symbol error paths.
|
||||
func TestAssembleFileErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string // substring of the error
|
||||
}{
|
||||
{
|
||||
"undefined symbol",
|
||||
`
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
VMOVDQU nope<>(SB), X0
|
||||
RET
|
||||
`,
|
||||
"undefined symbol",
|
||||
},
|
||||
{
|
||||
"DATA without GLOBL",
|
||||
`
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
RET
|
||||
DATA orphan<>+0(SB)/4, $1
|
||||
`,
|
||||
"no matching GLOBL",
|
||||
},
|
||||
{
|
||||
"DATA exceeds size",
|
||||
`
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
RET
|
||||
GLOBL tiny<>(SB), RODATA, $4
|
||||
DATA tiny<>+0(SB)/8, $1
|
||||
`,
|
||||
"exceeds GLOBL size",
|
||||
},
|
||||
{
|
||||
"DATA bad width",
|
||||
`
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
RET
|
||||
GLOBL odd<>(SB), RODATA, $4
|
||||
DATA odd<>+0(SB)/3, $1
|
||||
`,
|
||||
"invalid width",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
f, errs := parser.Parse("e_amd64.s", c.src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("%s: parse: %v", c.name, errs)
|
||||
}
|
||||
if _, err := AssembleFile(f); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: error %v, want substring %q", c.name, err, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
// A static-symbol operand is unresolvable in single-function assembly.
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
MOVQ x<>(SB), AX
|
||||
RET
|
||||
GLOBL x<>(SB), RODATA, $8
|
||||
DATA x<>+0(SB)/4, $1
|
||||
`)
|
||||
if _, _, err := Assemble(fn); err == nil || !strings.Contains(err.Error(), "file-level assembly") {
|
||||
t.Errorf("single-function SB: error %v, want a file-level-assembly error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel —
|
||||
// all functions plus the file-local mask24 constant — and checks that every
|
||||
// static-symbol load resolves to the right bytes in the image. Skipped when
|
||||
// the sibling repository is not checked out.
|
||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 17 {
|
||||
t.Errorf("functions = %d, want 17", len(img.Funcs))
|
||||
}
|
||||
|
||||
// mask24 as the DATA directives define it.
|
||||
mask := []byte{
|
||||
0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80,
|
||||
0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80,
|
||||
}
|
||||
image := img.Bytes()
|
||||
if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) {
|
||||
t.Errorf("mask24 contents %x, want %x", got, mask)
|
||||
}
|
||||
|
||||
// Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU
|
||||
// with a RIP-relative r/m) must land on the mask bytes within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
for pc := 0; pc < len(code); {
|
||||
inst, err := x86asm.Decode(code[pc:], 64)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err)
|
||||
}
|
||||
// mod=00, rm=101 → RIP-relative.
|
||||
if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 {
|
||||
rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24)
|
||||
target := fn.Offset + pc + 8 + int(rel)
|
||||
if !bytes.Equal(image[target:target+16], mask) {
|
||||
t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask)
|
||||
}
|
||||
loads++
|
||||
}
|
||||
pc += inst.Len
|
||||
}
|
||||
}
|
||||
if loads != 2 {
|
||||
t.Errorf("mask loads found = %d, want 2", loads)
|
||||
}
|
||||
}
|
||||
@@ -41,3 +41,15 @@ func Idx(base, index Reg, scale int, disp int64, size int) Mem {
|
||||
func Rip(disp int64, size int) Mem {
|
||||
return Mem{Disp: disp, Size: size}
|
||||
}
|
||||
|
||||
// sbMem is a memory operand that references a static (SB) symbol. It encodes
|
||||
// as a RIP-relative reference with a placeholder displacement; the encoder
|
||||
// records a patch site so the file-level layout can fill in the true rel32
|
||||
// once the symbol's address is known.
|
||||
type sbMem struct {
|
||||
size int
|
||||
name string // static symbol name (the GLOBL identifier)
|
||||
addend int64 // byte offset within the symbol
|
||||
}
|
||||
|
||||
func (sbMem) isOperand() {}
|
||||
|
||||
+13
-2
@@ -507,7 +507,8 @@ func vecReg(op Operand) (Reg, bool) {
|
||||
|
||||
// vecOrMem reports whether op is a vector register or a memory reference.
|
||||
func vecOrMem(op Operand) bool {
|
||||
if _, ok := op.(Mem); ok {
|
||||
switch op.(type) {
|
||||
case Mem, sbMem:
|
||||
return true
|
||||
}
|
||||
r, ok := op.(Reg)
|
||||
@@ -518,7 +519,7 @@ func vecOrMem(op Operand) bool {
|
||||
// acceptable: memory always is, a GPR only for VMOVD/VMOVQ.
|
||||
func validMoveOther(ms vexMoveSpec, op Operand) bool {
|
||||
switch o := op.(type) {
|
||||
case Mem:
|
||||
case Mem, sbMem:
|
||||
return true
|
||||
case Reg:
|
||||
return ms.gprOK && !o.isVec()
|
||||
@@ -533,6 +534,7 @@ func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Ope
|
||||
var modrm, sib int
|
||||
var disp []byte
|
||||
var xBit, bBit int
|
||||
var sb *sbRef
|
||||
switch r := rm.(type) {
|
||||
case Reg:
|
||||
modrm = 0xC0 | regField<<3 | (r.idx & 7)
|
||||
@@ -546,6 +548,12 @@ func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Ope
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case sbMem:
|
||||
// RIP-relative static-symbol reference; disp32 patched at link time.
|
||||
modrm = regField<<3 | 0x05
|
||||
sib = -1
|
||||
disp = le32(0)
|
||||
sb = &sbRef{name: r.name, addend: r.addend}
|
||||
default:
|
||||
return fmt.Errorf("invalid VEX r/m operand")
|
||||
}
|
||||
@@ -561,6 +569,9 @@ func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Ope
|
||||
if sib >= 0 {
|
||||
e.out = append(e.out, byte(sib))
|
||||
}
|
||||
if sb != nil {
|
||||
e.patches = append(e.patches, encPatch{off: len(e.out), name: sb.name, addend: sb.addend})
|
||||
}
|
||||
e.out = append(e.out, disp...)
|
||||
return nil
|
||||
}
|
||||
|
||||
+35
-16
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
// version is the release version, stamped at build time via
|
||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||
var version = "0.3.0"
|
||||
var version = "0.4.0"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
@@ -238,20 +238,18 @@ func cmdAsm(args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
var all []byte
|
||||
functions := 0
|
||||
for _, d := range f.Decls {
|
||||
txt, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, _, err := asm.Assemble(txt)
|
||||
img, err := asm.AssembleFile(f)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: %s: %v\n", path, txt.Name.Name, err)
|
||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
||||
return 1
|
||||
}
|
||||
functions++
|
||||
fmt.Printf("%s: %d bytes\n", txt.Name.Name, len(code))
|
||||
if len(img.Funcs) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found")
|
||||
return 1
|
||||
}
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
fmt.Printf("%s: %d bytes\n", fn.Name, fn.Size)
|
||||
for i := 0; i < len(code); i += 16 {
|
||||
end := i + 16
|
||||
if end > len(code) {
|
||||
@@ -263,13 +261,34 @@ func cmdAsm(args []string) int {
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
all = append(all, code...)
|
||||
}
|
||||
if functions == 0 {
|
||||
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found")
|
||||
return 1
|
||||
if len(img.Data) > 0 {
|
||||
fmt.Printf("data: %d bytes at 0x%x\n", len(img.Data), len(img.Code))
|
||||
for _, d := range f.Decls {
|
||||
g, ok := d.(*ast.Globl)
|
||||
if !ok || g.Name == nil || g.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
size := 0
|
||||
if g.Size != nil && g.Size.Imm.HasVal {
|
||||
size = int(g.Size.Imm.Val)
|
||||
}
|
||||
fmt.Printf(" %s: %d bytes at 0x%x\n", g.Name.Name, size, img.Symbols[g.Name.Name])
|
||||
}
|
||||
for i := 0; i < len(img.Data); i += 16 {
|
||||
end := i + 16
|
||||
if end > len(img.Data) {
|
||||
end = len(img.Data)
|
||||
}
|
||||
fmt.Printf(" %04x:", len(img.Code)+i)
|
||||
for _, b := range img.Data[i:end] {
|
||||
fmt.Printf(" %02x", b)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
if *out != "" {
|
||||
all := img.Bytes()
|
||||
if err := os.WriteFile(*out, all, 0o644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm asm:", err)
|
||||
return 1
|
||||
|
||||
+15
-9
@@ -206,15 +206,21 @@ memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
|
||||
`VMULPD`, `VXORPD`, `VUNPCKHPD`, the scalar `VADDSD`/`VMULSD`, `VCVTDQ2PD`,
|
||||
`VFMADD231PD`) and the no-operand `VZEROUPPER` — together with `VPERMD` and
|
||||
the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves,
|
||||
`CVTSx2SD`, `IMUL3`), covering every instruction the go-flac AVX2 kernels use
|
||||
apart from global-symbol loads. Every encoding is validated two ways: by
|
||||
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against the
|
||||
machine code the real Go assembler emits — a comparison that now holds for
|
||||
whole functions: every kernel function that avoids `SB` operands assembles to
|
||||
exactly the Go toolchain's bytes. This increment covers register / memory /
|
||||
immediate / FP-frame operands, local-label jumps and these VEX SIMD forms;
|
||||
EVEX / AVX-512, `SB` (global symbol) operands (relocations) and object-file
|
||||
emission are the rest of Phase 2.
|
||||
`CVTSx2SD`, `IMUL3`), covering every instruction the go-flac AVX2 kernels
|
||||
use. Every encoding is validated two ways: by round-trip decoding through
|
||||
`golang.org/x/arch`, and byte-for-byte against the machine code the real Go
|
||||
assembler emits — a comparison that holds for the whole kernel: all 17
|
||||
functions of the go-flac AVX2 file assemble to exactly the Go toolchain's
|
||||
bytes, the lone exception being the displacements of the static-constant
|
||||
loads, which the Go linker fills at link time.
|
||||
|
||||
File-level assembly (`AssembleFile`) goes beyond single functions: it
|
||||
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
|
||||
behind the code and resolves references to them (`mask<>(SB)`) to
|
||||
RIP-relative loads whose displacements point inside the resulting image, so
|
||||
the bytes are self-consistent at any base address. External (non-file-local)
|
||||
symbols are rejected: they need object-file emission, which — together with
|
||||
EVEX / AVX-512 and the other architectures — is the rest of Phase 2.
|
||||
|
||||
## Extension points
|
||||
|
||||
|
||||
Reference in New Issue
Block a user