Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0920edb092 | ||
|
|
900c9772b1 |
+43
-10
@@ -23,16 +23,19 @@ import (
|
||||
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
|
||||
// integer and shuffle/extract/permute/move set is in.
|
||||
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
code, _, labels, err := assemble(t, nil)
|
||||
code, _, labels, _, err := assemble(t, nil)
|
||||
return code, labels, err
|
||||
}
|
||||
|
||||
// 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).
|
||||
// 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 {
|
||||
symbols map[string]bool
|
||||
symbols map[string]bool
|
||||
allowExternal bool
|
||||
}
|
||||
|
||||
// sbPatch is a function-relative static-symbol relocation: the disp32 field
|
||||
@@ -45,9 +48,19 @@ type sbPatch struct {
|
||||
addend int64
|
||||
}
|
||||
|
||||
// spadjStep is one stack-adjustment boundary within a function: Value is the
|
||||
// SP delta from the entry state (just below the return address) in effect
|
||||
// from PC (function-relative) until the next step. The steps feed the
|
||||
// pcsp table of the object-file emitters.
|
||||
type spadjStep struct {
|
||||
pc int
|
||||
value int
|
||||
}
|
||||
|
||||
// assemble encodes a TEXT body, returning the machine code, the static-symbol
|
||||
// patch sites (for the file-level layout to resolve) and the label table.
|
||||
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, error) {
|
||||
// patch sites (for the file-level layout to resolve), the label table and the
|
||||
// stack-adjustment boundaries.
|
||||
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, error) {
|
||||
fi := computeFrame(t)
|
||||
chain := jumpChain(t)
|
||||
resolve := func(name string) string {
|
||||
@@ -71,7 +84,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e
|
||||
case *ast.Instr:
|
||||
sz, err := instrSize(s, fi, long[i], link)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
}
|
||||
sizes[i] = sz
|
||||
pcs[i] = pos
|
||||
@@ -111,24 +124,42 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e
|
||||
// Pass 2: emit.
|
||||
out := append([]byte(nil), fi.prologue...)
|
||||
var patches []sbPatch
|
||||
var steps []spadjStep
|
||||
if fi.useFP {
|
||||
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
|
||||
// changes nothing; SUBQ $size, SP completes the frame.
|
||||
steps = append(steps,
|
||||
spadjStep{1, 8},
|
||||
spadjStep{len(fi.prologue), 8 + fi.size},
|
||||
)
|
||||
}
|
||||
pos := len(fi.prologue)
|
||||
for i, stmt := range t.Body {
|
||||
s, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.ToUpper(s.Mnemonic.Text) == "RET" && fi.useFP {
|
||||
// The RET's epilogue prefix unwinds: ADDQ $size, SP restores
|
||||
// the saved-BP-only stack, POPQ BP the entry state.
|
||||
epi := len(fi.epilogue)
|
||||
steps = append(steps,
|
||||
spadjStep{pos + epi - 1, 8},
|
||||
spadjStep{pos + epi, 0},
|
||||
)
|
||||
}
|
||||
code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
}
|
||||
if len(code) != sizes[i] {
|
||||
return nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||
}
|
||||
patches = append(patches, ps...)
|
||||
out = append(out, code...)
|
||||
pos += len(code)
|
||||
}
|
||||
return out, patches, offsets, nil
|
||||
return out, patches, offsets, steps, nil
|
||||
}
|
||||
|
||||
// jumpChain precomputes jump-to-jump folding: a label whose first instruction
|
||||
@@ -425,7 +456,9 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Op
|
||||
if a.Sym.Static {
|
||||
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
|
||||
}
|
||||
|
||||
+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)
|
||||
}
|
||||
}
|
||||
+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 (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"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.
|
||||
// followed by the file's static data section (GLOBL/DATA). References to
|
||||
// file-local static symbols are encoded RIP-relative and resolved within the
|
||||
// image, so the raw bytes are self-consistent and executable at any base
|
||||
// address; references to external symbols are recorded as relocations
|
||||
// (Funcs[i].Relocs, Externals) and left unresolved — the object-file
|
||||
// emitters turn them into linker relocations.
|
||||
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
|
||||
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
|
||||
DataSyms []DataSymbol // GLOBL symbols, in layout order
|
||||
Externals []string // referenced but undefined symbols, sorted
|
||||
}
|
||||
|
||||
// FuncLayout describes one assembled function within an Image.
|
||||
type FuncLayout struct {
|
||||
Name string
|
||||
Pkg string // explicit package prefix ("" = the current package)
|
||||
Static bool // the <> marker: file-local, not exported
|
||||
Offset int // start offset within the image (== offset within Code)
|
||||
Size int
|
||||
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
|
||||
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
|
||||
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.
|
||||
@@ -37,19 +83,21 @@ func (img *Image) Bytes() []byte {
|
||||
|
||||
// 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.
|
||||
// reference to a file-local static symbol becomes a RIP-relative load whose
|
||||
// displacement is resolved against that layout; a reference to a symbol no
|
||||
// GLOBL defines is recorded as an external relocation (Externals) with its
|
||||
// displacement left zero — the object-file emitters resolve it at link
|
||||
// time, while the raw image (Bytes) cannot represent it.
|
||||
func AssembleFile(f *ast.File) (*Image, error) {
|
||||
syms, order, err := collectData(f)
|
||||
dataSyms, err := collectData(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
known := make(map[string]bool, len(syms))
|
||||
for name := range syms {
|
||||
known[name] = true
|
||||
known := make(map[string]bool, len(dataSyms))
|
||||
for _, d := range dataSyms {
|
||||
known[d.name] = true
|
||||
}
|
||||
link := &linkInfo{symbols: known}
|
||||
link := &linkInfo{symbols: known, allowExternal: true}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
type asmFunc struct {
|
||||
@@ -62,50 +110,100 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, patches, labels, err := assemble(t, link)
|
||||
code, patches, labels, steps, err := assemble(t, link)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
img.Funcs = append(img.Funcs, FuncLayout{
|
||||
fl := FuncLayout{
|
||||
Name: t.Name.Name,
|
||||
Pkg: t.Name.Pkg,
|
||||
Static: t.Name.Static,
|
||||
Offset: len(img.Code),
|
||||
Size: len(code),
|
||||
Frame: frameSize(t),
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
})
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
switch f {
|
||||
case "NOSPLIT":
|
||||
fl.NoSplit = true
|
||||
case "SPWRITE":
|
||||
fl.SPWrite = true
|
||||
}
|
||||
}
|
||||
for _, s := range steps {
|
||||
fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value})
|
||||
}
|
||||
img.Funcs = append(img.Funcs, fl)
|
||||
img.Code = append(img.Code, code...)
|
||||
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
||||
}
|
||||
|
||||
// Lay out the data section behind the code, each symbol 16-aligned.
|
||||
dataStart := len(img.Code)
|
||||
for _, name := range order {
|
||||
for _, d := range dataSyms {
|
||||
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]...)
|
||||
img.Symbols[d.name] = dataStart + len(img.Data)
|
||||
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 {
|
||||
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)
|
||||
reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend}
|
||||
if imgOff, ok := img.Symbols[p.name]; ok {
|
||||
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
||||
if rel < -1<<31 || rel >= 1<<31 {
|
||||
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
||||
}
|
||||
copy(code[p.off:p.off+4], le32(rel))
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
func collectData(f *ast.File) ([]dataSym, error) {
|
||||
index := map[string]int{}
|
||||
var syms []dataSym
|
||||
for _, d := range f.Decls {
|
||||
switch dd := d.(type) {
|
||||
case *ast.Globl:
|
||||
@@ -113,50 +211,88 @@ func collectData(f *ast.File) (map[string][]byte, []string, error) {
|
||||
continue
|
||||
}
|
||||
name := dd.Name.Name
|
||||
if _, dup := syms[name]; dup {
|
||||
return nil, nil, fmt.Errorf("duplicate GLOBL %q", name)
|
||||
if _, dup := index[name]; dup {
|
||||
return 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)
|
||||
index[name] = len(syms)
|
||||
ds := dataSym{
|
||||
name: name,
|
||||
pkg: dd.Name.Pkg,
|
||||
buf: make([]byte, size),
|
||||
static: dd.Name.Static,
|
||||
}
|
||||
for _, f := range dd.Flags {
|
||||
switch f {
|
||||
case "RODATA":
|
||||
ds.rodata = true
|
||||
case "DUPOK":
|
||||
ds.dupok = true
|
||||
case "1":
|
||||
ds.dupok = true
|
||||
case "8":
|
||||
ds.rodata = true
|
||||
case "9":
|
||||
ds.dupok = true
|
||||
ds.rodata = true
|
||||
}
|
||||
}
|
||||
syms = append(syms, ds)
|
||||
|
||||
case *ast.Data:
|
||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
buf, ok := syms[dd.Name.Name]
|
||||
i, ok := index[dd.Name.Name]
|
||||
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 {
|
||||
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
|
||||
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)
|
||||
return nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w)
|
||||
}
|
||||
off := dd.Name.Offset
|
||||
buf := syms[i].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
|
||||
if dd.Value.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
for i := 0; i < w; i++ {
|
||||
buf[off+int64(i)] = byte(v >> (8 * i))
|
||||
for j := 0; j < w; j++ {
|
||||
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.
|
||||
func align16(n int) int {
|
||||
return (n + 15) &^ 15
|
||||
}
|
||||
|
||||
// frameSize returns the local frame size declared on the TEXT directive.
|
||||
func frameSize(t *ast.Text) int {
|
||||
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||
return int(t.Frame.Imm.Val)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// argsSize returns the argument/result area declared on the TEXT directive.
|
||||
func argsSize(t *ast.Text) int {
|
||||
if t.Args != nil && t.Args.Imm.HasVal {
|
||||
return int(t.Args.Imm.Val)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
+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)
|
||||
}
|
||||
}
|
||||
+46
-9
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
// version is the release version, stamped at build time via
|
||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||
var version = "0.10.0"
|
||||
var version = "0.12.0"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
@@ -93,6 +93,8 @@ 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)
|
||||
}
|
||||
|
||||
@@ -339,17 +341,26 @@ hover, document symbols, diagnostics and semantic-token highlighting.
|
||||
}
|
||||
|
||||
func cmdAsm(args []string) int {
|
||||
fs := newCommand("asm", "gasm asm [-o out.bin] <file>", `
|
||||
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", `
|
||||
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
|
||||
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
|
||||
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
|
||||
resolved RIP-relative — and printed as a hex dump. With -o the concatenated
|
||||
image (functions followed by the data section) is written to a file instead.
|
||||
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 concatenated machine code to this file")
|
||||
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)
|
||||
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
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
@@ -420,12 +431,38 @@ image (functions followed by the data section) is written to a file instead.
|
||||
}
|
||||
}
|
||||
if *out != "" {
|
||||
all := img.Bytes()
|
||||
if err := os.WriteFile(*out, all, 0o644); err != nil {
|
||||
var obj []byte
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
+20
-4
@@ -243,10 +243,26 @@ 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
|
||||
the remaining EVEX forms and the other architectures — is the rest of
|
||||
Phase 2.
|
||||
the bytes are self-consistent at any base address. References to symbols no
|
||||
`GLOBL` defines are kept as relocations on the function layout, and the
|
||||
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
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||
|
||||
version := "0.10.0"
|
||||
version := "0.12.0"
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
@@ -375,6 +375,11 @@ func parseImmediate(g []token.Token) ast.Immediate {
|
||||
if v, ok := tryInt(text); ok {
|
||||
imm.Val = v
|
||||
imm.HasVal = true
|
||||
} else if u, err := strconv.ParseUint(text, 0, 64); err == nil && !imm.Neg {
|
||||
// Unsigned 64-bit literals (DATA mask<>+8(SB)/8, $0x8000…)
|
||||
// overflow int64; keep the bit pattern.
|
||||
imm.Val = int64(u)
|
||||
imm.HasVal = true
|
||||
} else {
|
||||
imm.Float = text
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user