From 900c9772b17d9cac5f11a3a1cdcd30710256f399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 16 Jul 2026 20:52:20 +0200 Subject: [PATCH] feat(asm): emit linkable ELF and Mach-O objects with external symbols Assisted-by: Qwen 3.8 Max Preview --- asm/assemble.go | 11 +- asm/elf.go | 301 +++++++++++++++++++++++++++++++++++++++++ asm/elf_test.go | 310 +++++++++++++++++++++++++++++++++++++++++++ asm/link.go | 148 +++++++++++++++------ asm/macho.go | 258 +++++++++++++++++++++++++++++++++++ asm/macho_test.go | 127 ++++++++++++++++++ cmd/gasm/main.go | 49 +++++-- docs/ARCHITECTURE.md | 15 ++- justfile | 2 +- 9 files changed, 1165 insertions(+), 56 deletions(-) create mode 100644 asm/elf.go create mode 100644 asm/elf_test.go create mode 100644 asm/macho.go create mode 100644 asm/macho_test.go diff --git a/asm/assemble.go b/asm/assemble.go index 0964cab..ecab68e 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -30,9 +30,12 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) { // 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 @@ -425,7 +428,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 } diff --git a/asm/elf.go b/asm/elf.go new file mode 100644 index 0000000..f40dc0a --- /dev/null +++ b/asm/elf.go @@ -0,0 +1,301 @@ +// Copyright (c) 2026 Petr Balvín (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 } diff --git a/asm/elf_test.go b/asm/elf_test.go new file mode 100644 index 0000000..7316294 --- /dev/null +++ b/asm/elf_test.go @@ -0,0 +1,310 @@ +// Copyright (c) 2026 Petr Balvín (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 + +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) + } +} diff --git a/asm/link.go b/asm/link.go index b65a3ec..676f32a 100644 --- a/asm/link.go +++ b/asm/link.go @@ -5,27 +5,58 @@ 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 - Offset int // start offset within the image (== offset within Code) + Pkg string // explicit package prefix ("" = the current package) + Static bool // the <> marker: file-local, not exported + Offset int // start offset within the image (== offset within Code) Size int Labels map[string]int // local labels, function-relative + Relocs []Reloc // static-symbol references, in emission order +} + +// 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 + Pkg string // explicit package prefix ("" = the current package) + Offset int // byte offset within Data + Size int + Static bool // the <> marker: file-local, not exported } // Bytes returns the whole image: code, then data. @@ -37,19 +68,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 { @@ -68,6 +101,8 @@ func AssembleFile(f *ast.File) (*Image, error) { } img.Funcs = append(img.Funcs, FuncLayout{ Name: t.Name.Name, + Pkg: t.Name.Pkg, + Static: t.Name.Static, Offset: len(img.Code), Size: len(code), Labels: labels, @@ -78,34 +113,63 @@ func AssembleFile(f *ast.File) (*Image, error) { // 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, + }) + 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 +} + // 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,47 +177,53 @@ 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) + syms = append(syms, dataSym{ + name: name, + pkg: dd.Name.Pkg, + buf: make([]byte, size), + static: dd.Name.Static, + }) case *ast.Data: if dd.Name == nil || dd.Name.Pseudo != "SB" { continue } - 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. diff --git a/asm/macho.go b/asm/macho.go new file mode 100644 index 0000000..00efbed --- /dev/null +++ b/asm/macho.go @@ -0,0 +1,258 @@ +// Copyright (c) 2026 Petr Balvín (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 +} diff --git a/asm/macho_test.go b/asm/macho_test.go new file mode 100644 index 0000000..d8602f8 --- /dev/null +++ b/asm/macho_test.go @@ -0,0 +1,127 @@ +// Copyright (c) 2026 Petr Balvín (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) + } +} diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index b7f3afb..5b6034b 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -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.11.0" func main() { if len(os.Args) < 2 { @@ -93,6 +93,7 @@ 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 `, version) } @@ -339,17 +340,24 @@ hover, document symbols, diagnostics and semantic-token highlighting. } func cmdAsm(args []string) int { - fs := newCommand("asm", "gasm asm [-o out.bin] ", ` + fs := newCommand("asm", "gasm asm [--format raw|elf|macho] [-o out] ", ` 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 — references +to symbols no GLOBL in the file defines become undefined external symbols. `) - 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 or macho (relocatable object)") fs.Parse(args) if fs.NArg() != 1 { - fmt.Fprintln(os.Stderr, "usage: gasm asm [-o out.bin] ") + fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho] [-o out] ") return 2 } path := fs.Arg(0) @@ -420,12 +428,35 @@ 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" + default: + fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf or macho)\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 } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 98a889a..7c490d1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -243,10 +243,17 @@ 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. GOOBJ +emission, the format the Go linker consumes directly, is the remaining +piece of Phase 2 (together with the rest of the EVEX set and the other +architectures). ## Extension points diff --git a/justfile b/justfile index eb6c862..ba07edb 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.10.0" +version := "0.11.0" default: @just --list