feat: drop Mach-O and macOS support, Linux-only
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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) |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user