feat: drop Mach-O and macOS support, Linux-only

This commit is contained in:
2026-08-07 22:20:26 +02:00
parent 30c53565a7
commit d7ee1b78d4
9 changed files with 41 additions and 504 deletions
+5
View File
@@ -9,6 +9,11 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
Unreleased changes on the `development` branch. Unreleased changes on the `development` branch.
### Changed
- **Linux only.** The toolkit, its CI and the released binaries are now
Linux-only; cross-compiled to linux/{amd64,arm64,riscv64,loong64}.
## [0.29.0] — 2026-08-07 ## [0.29.0] — 2026-08-07
RISC-V GOOBJ emission, YMM vector register display, named buffer allocation RISC-V GOOBJ emission, YMM vector register display, named buffer allocation
+2 -2
View File
@@ -4,7 +4,7 @@
- Go 1.26 or later (`toolchain go1.26.5`) - Go 1.26 or later (`toolchain go1.26.5`)
- `just` command runner - `just` command runner
- A Linux, FreeBSD, or macOS host on amd64 or arm64 - A Linux host on amd64, arm64, riscv64 or loong64
## Development Setup ## Development Setup
@@ -85,7 +85,7 @@ Attribute agent authorship in issues and pull requests on one trailing
line: line:
``` ```
_Assisted-by: DeepSeek V4 Pro_ _Assisted-by: Qwen 3.8 Max_
``` ```
## Questions ## Questions
+23 -24
View File
@@ -28,7 +28,7 @@ gasm profile show basic-block structure of functions
> **Status: Phase 4 — done, Phase 5 underway.** Phase 1 (the language > **Status: Phase 4 — done, Phase 5 underway.** Phase 1 (the language
> foundation, linter, formatter and language server) shipped in v0.1.0; > foundation, linter, formatter and language server) shipped in v0.1.0;
> Phase 2 (the standalone assembler — the full amd64 instruction set plus > Phase 2 (the standalone assembler — the full amd64 instruction set plus
> ELF, Mach-O and GOOBJ object emission) in v0.12.0; Phase 3 (dynamic > ELF and GOOBJ object emission) in v0.12.0; Phase 3 (dynamic
> analysis — JIT execution, differential testing, ABI checks and coverage > analysis — JIT execution, differential testing, ABI checks and coverage
> profiling) in v0.25.0; Phase 4 (interactive debugger — ptrace-based, > profiling) in v0.25.0; Phase 4 (interactive debugger — ptrace-based,
> breakpoints, watchpoints, stepping, vector register display, named buffer > breakpoints, watchpoints, stepping, vector register display, named buffer
@@ -56,9 +56,13 @@ carries the traditional conditional-jump spellings (`JZ`, `JNZ`, `JA`, `JC`,
command — `just gen` — and requires only a Go installation; the committed command — `just gen` — and requires only a Go installation; the committed
output has no runtime dependency on the toolchain. output has no runtime dependency on the toolchain.
The target *architectures* above are what the toolkit analyses. The toolkit ## Supported Platforms
itself is portable Go and builds on Linux, FreeBSD and macOS, on amd64 and
arm64 hosts. The toolkit runs on Linux. All four Linux architectures are supported as
hosts — amd64, arm64, riscv64 and loong64 — and the release matrix
cross-compiles the same four targets.
**FreeBSD support is planned for a future release.**
## Roadmap ## Roadmap
@@ -133,14 +137,14 @@ Done so far:
stack deltas, `pcfile`, `pcline`, `pcinline`). Verified end-to-end by stack deltas, `pcfile`, `pcline`, `pcinline`). Verified end-to-end by
swapping a gasm-emitted object into a `go build` in place of the swapping a gasm-emitted object into a `go build` in place of the
toolchain's, linking and running — bit-identical behaviour. toolchain's, linking and running — bit-identical behaviour.
- **Object-file emission** — `gasm asm --format elf` / `--format macho` - **Object-file emission** — `gasm asm --format elf` writes a relocatable
writes a relocatable object (a `.text` and a `.data` section, a symbol object (a `.text` and a `.data` section, a symbol table — file-local `<>`
table — file-local `<>` symbols local, the rest global — and one symbols local, the rest global — and one `R_X86_64_PC32` relocation per
`R_X86_64_PC32` / `X86_64_RELOC_SIGNED` relocation per static-symbol static-symbol reference) that links with the system toolchain: external
reference) that links with the system toolchain: external references references resolve against undefined symbols, file-local ones against the
resolve against undefined symbols, file-local ones against the data data section. Verified end-to-end by linking a gasm-emitted object with
section. Verified end-to-end by linking a gasm-emitted object with a C a C driver and running it. RISC-V uses the equivalent `R_RISCV_PCREL_HI20`
driver and running it. / `R_RISCV_PCREL_LO12_I` pair for AUIPC+JAL/JALR sequences.
- **`FP`/`SP` frame mapping** — the pseudo-registers are translated onto the - **`FP`/`SP` frame mapping** — the pseudo-registers are translated onto the
hardware stack pointer (`x+N(FP)` → `(N+8)(SP)` for a zero frame, `(N+frame+ hardware stack pointer (`x+N(FP)` → `(N+8)(SP)` for a zero frame, `(N+frame+
16)(SP)` with a frame pointer; locals via `x-N(SP)`), and the Go-style 16)(SP)` with a frame pointer; locals via `x-N(SP)`), and the Go-style
@@ -256,8 +260,7 @@ portable Go implementation every kernel is derived from.
label resolution, breakpoint management via `/proc/pid/mem`, named label resolution, breakpoint management via `/proc/pid/mem`, named
buffer allocation with pattern filling (`--buf`), and an interactive REPL. buffer allocation with pattern filling (`--buf`), and an interactive REPL.
- **Remaining:** disassembly at PC (x86asm decode), memory-write support, - **Remaining:** disassembly at PC (x86asm decode), memory-write support,
watchpoints, source-line mapping, and multi-platform support watchpoints, and source-line mapping.
(FreeBSD/macOS ptrace variants).
### Phase 5 — the other architectures · *in progress* ### Phase 5 — the other architectures · *in progress*
@@ -277,8 +280,9 @@ portable Go implementation every kernel is derived from.
dependency, `golang.org/x/arch`, is used **only in tests** to validate the dependency, `golang.org/x/arch`, is used **only in tests** to validate the
instruction encoder by round-trip decoding — it is never linked into the instruction encoder by round-trip decoding — it is never linked into the
`gasm` binary. `gasm` binary.
- **Portable.** Builds and runs on Linux, FreeBSD and macOS; amd64 and arm64 - **Linux-only.** Runs natively on amd64, arm64, riscv64 and loong64 Linux
hosts. Latest stable Go only. hosts; the release matrix cross-compiles the same four targets. Latest
stable Go only.
- **No vendor lock-in.** The integration surface is the Language Server - **No vendor lock-in.** The integration surface is the Language Server
Protocol and a command-line interface — both open standards. No cloud Protocol and a command-line interface — both open standards. No cloud
service, no proprietary API, no dependence on any one editor's internals. service, no proprietary API, no dependence on any one editor's internals.
@@ -297,7 +301,7 @@ portable Go implementation every kernel is derived from.
| `arch` | amd64, arm64, riscv64 and loong64 register files and instruction tables. | | `arch` | amd64, arm64, riscv64 and loong64 register files and instruction tables. |
| `lint` | Conservative static checks. | | `lint` | Conservative static checks. |
| `format` | A canonical formatter — `gofmt` for assembly. | | `format` | A canonical formatter — `gofmt` for assembly. |
| `asm` | The standalone assembler: amd64 and RISC-V encoders, linker, object-file emitters (ELF, Mach-O, GOOBJ). | | `asm` | The standalone assembler: amd64 and RISC-V encoders, linker, object-file emitters (ELF, GOOBJ). |
| `verify` | JIT execution substrate for dynamic analysis, combined ABI+fuzz differential testing (Phase 3). | | `verify` | JIT execution substrate for dynamic analysis, combined ABI+fuzz differential testing (Phase 3). |
| `debug` | Interactive ptrace debugger with GPR/YMM register display and named buffer allocation (Phase 4). | | `debug` | Interactive ptrace debugger with GPR/YMM register display and named buffer allocation (Phase 4). |
| `lsp` | Language Server Protocol server. | | `lsp` | Language Server Protocol server. |
@@ -305,9 +309,8 @@ portable Go implementation every kernel is derived from.
| `_gen` | The generator that rebuilds the instruction tables from the Go toolchain. | | `_gen` | The generator that rebuilds the instruction tables from the Go toolchain. |
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design rationale and See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design rationale and
data flow, [`docs/ZED.md`](docs/ZED.md) for the editor-integration story, and data flow, and [`docs/DEFERRED.md`](docs/DEFERRED.md) for design decisions
[`docs/DEFERRED.md`](docs/DEFERRED.md) for design decisions deliberately deliberately postponed (with the analysis needed to pick them up again).
postponed (with the analysis needed to pick them up again).
## Quick start ## Quick start
@@ -353,10 +356,6 @@ binary and associate it with `.s` files. Syntax highlighting is delivered as
infers the target architecture from the file-name suffix infers the target architecture from the file-name suffix
(`_amd64.s` / `_arm64.s` / `_riscv64.s` / `_loong64.s`). (`_amd64.s` / `_arm64.s` / `_riscv64.s` / `_loong64.s`).
Zed users should read [`docs/ZED.md`](docs/ZED.md): Zed's native highlighting
engine (Tree-sitter, C/WASM) cannot be fed from pure Go, so the pure-Go path
into Zed is the language server and its semantic tokens.
## Licence ## Licence
BSD-3-Clause — the same licence as Go itself. See [`LICENSE`](LICENSE). BSD-3-Clause — the same licence as Go itself. See [`LICENSE`](LICENSE).
-258
View File
@@ -1,258 +0,0 @@
// 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
}
-127
View File
@@ -1,127 +0,0 @@
// 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)
}
}
+7 -10
View File
@@ -395,7 +395,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
} }
func cmdAsm(args []string) int { func cmdAsm(args []string) int {
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", ` fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>", `
Assemble FILE (amd64 or riscv64) without the Go toolchain: every TEXT function is Assemble FILE (amd64 or riscv64) without the Go toolchain: every TEXT function is
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions, 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) FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
@@ -403,18 +403,18 @@ resolved RIP-relative — and printed as a hex dump.
With -o the output is written to a file instead. The --format flag selects 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 what is written: raw (the default) concatenates the functions and the data
section into one self-consistent image; elf and macho emit a relocatable section into one self-consistent image; elf emits a relocatable object
object (.text/.data sections, a symbol table and one PC32 relocation per (.text/.data sections, a symbol table and one PC32 relocation per
static-symbol reference) that links with the system toolchain; goobj emits static-symbol reference) that links with the system toolchain; goobj emits
the Go toolchain's own object format, which cmd/link consumes directly (it the Go toolchain's own object format, which cmd/link consumes directly (it
requires -p, the package path, and the installed Go toolchain). requires -p, the package path, and the installed Go toolchain).
`) `)
out := fs.String("o", "", "write the output 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)") format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)")
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)") pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
fs.Parse(args) fs.Parse(args)
if fs.NArg() != 1 { if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>") fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>")
return 2 return 2
} }
path := fs.Arg(0) path := fs.Arg(0)
@@ -497,7 +497,7 @@ requires -p, the package path, and the installed Go toolchain).
switch *format { switch *format {
case "raw": case "raw":
if len(img.Externals) > 0 { 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]) fmt.Fprintf(os.Stderr, "gasm asm: external symbol %q needs an object file (use --format elf)\n", img.Externals[0])
return 1 return 1
} }
obj, kind = img.Bytes(), "raw image" obj, kind = img.Bytes(), "raw image"
@@ -508,9 +508,6 @@ requires -p, the package path, and the installed Go toolchain).
obj, err = img.ELFObject() obj, err = img.ELFObject()
} }
kind = "ELF object" kind = "ELF object"
case "macho":
obj, err = img.MachOObject()
kind = "Mach-O object"
case "goobj": case "goobj":
if targetArch == arch.RISCV { if targetArch == arch.RISCV {
obj, err = img.GOObjectRISCV(*pkg, path) obj, err = img.GOObjectRISCV(*pkg, path)
@@ -519,7 +516,7 @@ requires -p, the package path, and the installed Go toolchain).
} }
kind = "Go object" kind = "Go object"
default: default:
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho or goobj)\n", *format) fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf or goobj)\n", *format)
return 2 return 2
} }
if err != nil { if err != nil {
+2 -2
View File
@@ -275,8 +275,8 @@ RIP-relative loads whose displacements point inside the resulting image, so
the bytes are self-consistent at any base address. References to symbols no 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 `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 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 writer (`gasm asm --format elf`) lays the code and data out as `.text`/`.data`
as `.text`/`.data` (or `__text`/`__data`) sections, export a symbol per sections, exports a symbol per
`TEXT` and `GLOBL` (the `<>` ones local, the rest global) and emit one `TEXT` and `GLOBL` (the `<>` ones local, the rest global) and emit one
PC-relative relocation per static-symbol reference — undefined external PC-relative relocation per static-symbol reference — undefined external
symbols included, so the output links with the system toolchain. The GOOBJ symbols included, so the output links with the system toolchain. The GOOBJ
-79
View File
@@ -1,79 +0,0 @@
# Using gasm-devkit with Zed
This document is deliberately blunt, because the situation is a genuine
conflict between two of the project's own commitments, and papering over it
would be dishonest.
## The conflict
gasm-devkit is **pure Go, no C, no cgo, no JavaScript runtimes, no native
binaries, no vendor lock-in, no platform-specific IDE internals.**
Zed's extension model, as verified against Zed's own documentation, is:
- Extensions are written in **Rust** and compiled to **WebAssembly**
(`wasm32-wasip2`).
- Syntax highlighting is provided by **Tree-sitter** grammars, which are
**C** compiled to WebAssembly with the wasi-sdk, from a grammar written in a
**JavaScript** DSL.
- A *new* language cannot be registered through configuration alone. Defining
a language requires an extension, and every language extension must name a
Tree-sitter grammar. (Zed's `lsp` settings section configures
already-registered servers; it does not register an arbitrary external binary
for a brand-new language.)
There is therefore **no pure-Go path into Zed's extension host.** This is a
property of Zed, not of gasm-devkit: no language tooling author can feed Zed a
pure-Go highlighting grammar, because Zed's highlighting engine is Tree-sitter
and its plugin runtime is Rust/WASM.
## What gasm-devkit gives Zed regardless
The toolkit's integration surface is the **Language Server Protocol**, an open
standard. Through `gasm lsp` it provides, with zero editor-specific code:
- autocomplete (instructions, registers, pseudo-registers, labels),
- hover documentation,
- diagnostics (the linter, pushed as you type),
- document outline (functions and labels),
- **syntax highlighting, delivered as LSP semantic tokens.**
That last point matters: Zed can render highlighting entirely from LSP semantic
tokens (`"semantic_tokens": "full"` replaces Tree-sitter highlighting for a
language). So the highlighting *capability* exists in pure Go; what Zed needs
is merely to be told that `.s` files are a language served by `gasm lsp`.
## The honest options
1. **Use an editor that registers an external LSP by configuration.**
Neovim, Helix, VS Code and Sublime all let you associate `.s` with the
`gasm lsp` binary and use its semantic tokens — no Rust, no C, no lock-in.
This is the option that satisfies every stated constraint with no
exception.
2. **Treat a Zed adapter as one quarantined exception.** A minimal Zed
extension — a few lines of Rust that register the language and launch
`gasm lsp` — plus either a Tree-sitter grammar or `"full"` semantic tokens
for highlighting. Crucially, this adapter is the *editor's plugin format*;
it is sandboxed inside Zed and never linked into, compiled into, or shipped
with the Go toolkit. gasm-devkit itself stays pure Go. But producing it
uses the Rust/wasi-sdk/Tree-sitter toolchain, which the project constraints
forbid — so it must be a conscious, explicit decision, not a silent one.
The author's philosophy — digital sovereignty, no dependency on toolchains he
does not control — is the tie-breaker, and it is a value judgement rather than
a technical one. gasm-devkit is built so that **either** choice keeps the
toolkit itself clean: the pure-Go core and the LSP are the product; a Zed
adapter, if ever wanted, is a thin, separable leaf.
## Wiring the LSP (editor-agnostic)
Run the server and point an LSP client at it:
```sh
go run ./cmd/gasm lsp # or: go install ./cmd/gasm && gasm lsp
```
Associate the command with `*.s` (and `*_amd64.s` / `*_arm64.s`) in whichever
editor you use. The server infers the target architecture from the file-name
suffix and selects the amd64 or arm64 instruction tables accordingly.
+2 -2
View File
@@ -50,13 +50,13 @@ Rules: `unknown-instruction`, `operand-count`, `undefined-label`,
`abi-argsize`, `unreachable-code`, `register-clobber`, `abi-argsize`, `unreachable-code`, `register-clobber`,
`funcdata-pcdata`. `funcdata-pcdata`.
## `gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>` ## `gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>`
Assemble FILE (amd64) to machine code. Assemble FILE (amd64) to machine code.
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `--format` | Output format: `raw` (default), `elf`, `macho`, `goobj` | | `--format` | Output format: `raw` (default), `elf`, `goobj` |
| `-p` | Package path (required for `--format goobj`) | | `-p` | Package path (required for `--format goobj`) |
| `-o` | Write output to file (default: hex dump to stdout) | | `-o` | Write output to file (default: hex dump to stdout) |