519 lines
15 KiB
Go
519 lines
15 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
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). 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
|
|
DataSyms []DataSymbol // GLOBL symbols, in layout order
|
|
Externals []string // referenced but undefined symbols, sorted
|
|
}
|
|
|
|
// FuncLayout describes one assembled function within an Image.
|
|
type FuncLayout struct {
|
|
Name string
|
|
Pkg string // explicit package prefix ("" = the current package)
|
|
Static bool // the <> marker: file-local, not exported
|
|
Offset int // start offset within the image (== offset within Code)
|
|
Size int
|
|
Args int // declared argument/result area (the TEXT size suffix)
|
|
Frame int // local frame size (the TEXT $framesize)
|
|
NoSplit bool // the NOSPLIT flag
|
|
SPWrite bool // the SPWRITE flag: writes an arbitrary value to SP
|
|
Line int // source line of the TEXT directive
|
|
Labels map[string]int // local labels, function-relative
|
|
Relocs []Reloc // static-symbol references, in emission order
|
|
Spadj []SpadjStep // stack-adjustment boundaries, ascending by PC
|
|
Lines []LineEntry // source-line table: byte offset → source line
|
|
}
|
|
|
|
// SpadjStep is one stack-adjustment boundary: Value is the SP delta from the
|
|
// entry state in effect from PC (function-relative) until the next step.
|
|
type SpadjStep struct {
|
|
PC int
|
|
Value int
|
|
}
|
|
|
|
// LineEntry maps a byte offset (function-relative) to a source line number.
|
|
type LineEntry struct {
|
|
Offset int
|
|
Line int
|
|
}
|
|
|
|
// LineAt returns the source line number for the given function-relative byte
|
|
// offset, using a binary search on the line table. Returns 0 if the offset
|
|
// is before the first instruction or the table is empty.
|
|
func (fl *FuncLayout) LineAt(offset int) int {
|
|
if len(fl.Lines) == 0 {
|
|
return 0
|
|
}
|
|
// Binary search: find the last entry with Offset <= offset.
|
|
lo, hi := 0, len(fl.Lines)-1
|
|
for lo < hi {
|
|
mid := (lo + hi + 1) / 2
|
|
if fl.Lines[mid].Offset <= offset {
|
|
lo = mid
|
|
} else {
|
|
hi = mid - 1
|
|
}
|
|
}
|
|
if fl.Lines[lo].Offset <= offset {
|
|
return fl.Lines[lo].Line
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// 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.
|
|
// RelocKind discriminates the type of relocation needed.
|
|
type RelocKind int
|
|
|
|
const (
|
|
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
|
|
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
|
|
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
|
RelRISCVJal // R_RISCV_JAL (J-type call)
|
|
RelPCRelAbs // 32-bit absolute (R_RISCV_32)
|
|
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
|
|
RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st)
|
|
RelArm64Addr // R_ADDRARM64 (ADRP + ADD/LDR/STR pair)
|
|
RelArm64Branch // R_CALLARM64 (BL instruction)
|
|
)
|
|
|
|
type Reloc struct {
|
|
Off int
|
|
After int
|
|
Name string
|
|
Addend int64
|
|
External bool
|
|
Kind RelocKind
|
|
}
|
|
|
|
// 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
|
|
Rodata bool // the RODATA flag: read-only data
|
|
Dupok bool // the DUPOK flag: duplicate-OK
|
|
}
|
|
|
|
// Bytes returns the whole image: code, then data.
|
|
func (img *Image) Bytes() []byte {
|
|
out := make([]byte, 0, len(img.Code)+len(img.Data))
|
|
out = append(out, img.Code...)
|
|
return append(out, img.Data...)
|
|
}
|
|
|
|
// AssembleFile assembles every TEXT function of a parsed file and lays out
|
|
// its static symbols (GLOBL/DATA) in a data section behind the code. Each
|
|
// 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) {
|
|
dataSyms, err := collectData(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
known := make(map[string]bool, len(dataSyms))
|
|
for _, d := range dataSyms {
|
|
known[d.name] = true
|
|
}
|
|
link := &linkInfo{symbols: known, allowExternal: true}
|
|
|
|
img := &Image{Symbols: map[string]int{}}
|
|
type asmFunc struct {
|
|
name string
|
|
patches []sbPatch
|
|
}
|
|
var funcs []asmFunc
|
|
for _, d := range f.Decls {
|
|
t, ok := d.(*ast.Text)
|
|
if !ok {
|
|
continue
|
|
}
|
|
code, patches, labels, steps, lines, err := assemble(t, link)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
|
}
|
|
fl := FuncLayout{
|
|
Name: t.Name.Name,
|
|
Pkg: t.Name.Pkg,
|
|
Static: t.Name.Static,
|
|
Offset: len(img.Code),
|
|
Size: len(code),
|
|
Frame: frameSize(t),
|
|
Args: argsSize(t),
|
|
Line: t.Pos().Line,
|
|
Labels: labels,
|
|
Lines: lines,
|
|
}
|
|
for _, f := range t.Flags {
|
|
switch f {
|
|
case "NOSPLIT":
|
|
fl.NoSplit = true
|
|
case "SPWRITE":
|
|
fl.SPWrite = true
|
|
}
|
|
}
|
|
for _, s := range steps {
|
|
fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value})
|
|
}
|
|
img.Funcs = append(img.Funcs, fl)
|
|
img.Code = append(img.Code, code...)
|
|
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
|
}
|
|
|
|
// Lay out the data section behind the code, each symbol 16-aligned.
|
|
dataStart := len(img.Code)
|
|
for _, d := range dataSyms {
|
|
if pos := dataStart + len(img.Data); pos != align16(pos) {
|
|
img.Data = append(img.Data, make([]byte, align16(pos)-pos)...)
|
|
}
|
|
img.Symbols[d.name] = dataStart + len(img.Data)
|
|
img.DataSyms = append(img.DataSyms, DataSymbol{
|
|
Name: d.name,
|
|
Pkg: d.pkg,
|
|
Offset: len(img.Data),
|
|
Size: len(d.buf),
|
|
Static: d.static,
|
|
Rodata: d.rodata,
|
|
Dupok: d.dupok,
|
|
})
|
|
img.Data = append(img.Data, d.buf...)
|
|
}
|
|
|
|
// Resolve the RIP-relative displacements 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 {
|
|
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
|
|
}
|
|
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
|
|
}
|
|
|
|
// AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file
|
|
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
|
// code. SB references in the code are encoded as AUIPC pairs with zero
|
|
// immediates; the object-file emitters record relocations for the linker.
|
|
func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
|
dataSyms, err := collectData(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
img := &Image{Symbols: map[string]int{}}
|
|
for _, d := range f.Decls {
|
|
t, ok := d.(*ast.Text)
|
|
if !ok {
|
|
continue
|
|
}
|
|
code, labels, relocs, lines, spadj, err := assembleRISCV(t)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
|
}
|
|
fl := FuncLayout{
|
|
Name: t.Name.Name,
|
|
Pkg: t.Name.Pkg,
|
|
Static: t.Name.Static,
|
|
Offset: len(img.Code),
|
|
Size: len(code),
|
|
Frame: frameSize(t),
|
|
Args: argsSize(t),
|
|
Line: t.Pos().Line,
|
|
Labels: labels,
|
|
Lines: lines,
|
|
Spadj: spadj,
|
|
Relocs: relocs,
|
|
}
|
|
for _, f := range t.Flags {
|
|
switch f {
|
|
case "NOSPLIT":
|
|
fl.NoSplit = true
|
|
case "SPWRITE":
|
|
fl.SPWrite = true
|
|
}
|
|
}
|
|
img.Funcs = append(img.Funcs, fl)
|
|
img.Code = append(img.Code, code...)
|
|
}
|
|
|
|
// Lay out the data section behind the code, 16-aligned.
|
|
dataStart := len(img.Code)
|
|
for _, d := range dataSyms {
|
|
pos := dataStart + len(img.Data)
|
|
for pos%16 != 0 {
|
|
img.Data = append(img.Data, 0)
|
|
pos++
|
|
}
|
|
img.Symbols[d.name] = pos
|
|
img.Data = append(img.Data, d.buf...)
|
|
img.DataSyms = append(img.DataSyms, DataSymbol{
|
|
Name: d.name,
|
|
Pkg: d.pkg,
|
|
Offset: len(img.Data) - len(d.buf), // relative to the data section
|
|
Size: d.size,
|
|
Static: d.static,
|
|
Rodata: d.rodata,
|
|
Dupok: d.dupok,
|
|
})
|
|
}
|
|
|
|
markExternals(img, dataSyms)
|
|
return img, nil
|
|
}
|
|
|
|
// AssembleFileLOONG64 assembles every TEXT function of a parsed loong64 file
|
|
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
|
// code. SB references in the code are encoded as pcalau12i pairs with zero
|
|
// immediates; the object-file emitters record R_LOONG64_ADDR_HI/LO
|
|
// relocations for the linker.
|
|
func AssembleFileLOONG64(f *ast.File) (*Image, error) {
|
|
dataSyms, err := collectData(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
img := &Image{Symbols: map[string]int{}}
|
|
for _, d := range f.Decls {
|
|
t, ok := d.(*ast.Text)
|
|
if !ok {
|
|
continue
|
|
}
|
|
code, labels, relocs, lines, spadj, err := assembleLOONG64(t)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
|
}
|
|
fl := FuncLayout{
|
|
Name: t.Name.Name,
|
|
Pkg: t.Name.Pkg,
|
|
Static: t.Name.Static,
|
|
Offset: len(img.Code),
|
|
Size: len(code),
|
|
Frame: frameSize(t),
|
|
Args: argsSize(t),
|
|
Line: t.Pos().Line,
|
|
Labels: labels,
|
|
Lines: lines,
|
|
Spadj: spadj,
|
|
Relocs: relocs,
|
|
}
|
|
for _, f := range t.Flags {
|
|
switch f {
|
|
case "NOSPLIT":
|
|
fl.NoSplit = true
|
|
case "SPWRITE":
|
|
fl.SPWrite = true
|
|
}
|
|
}
|
|
img.Funcs = append(img.Funcs, fl)
|
|
img.Code = append(img.Code, code...)
|
|
}
|
|
|
|
// Lay out the data section behind the code, 16-aligned.
|
|
dataStart := len(img.Code)
|
|
for _, d := range dataSyms {
|
|
pos := dataStart + len(img.Data)
|
|
for pos%16 != 0 {
|
|
img.Data = append(img.Data, 0)
|
|
pos++
|
|
}
|
|
img.Symbols[d.name] = pos
|
|
img.Data = append(img.Data, d.buf...)
|
|
img.DataSyms = append(img.DataSyms, DataSymbol{
|
|
Name: d.name,
|
|
Pkg: d.pkg,
|
|
Offset: len(img.Data) - len(d.buf), // relative to the data section
|
|
Size: d.size,
|
|
Static: d.static,
|
|
Rodata: d.rodata,
|
|
Dupok: d.dupok,
|
|
})
|
|
}
|
|
|
|
markExternals(img, dataSyms)
|
|
return img, nil
|
|
}
|
|
|
|
// markExternals identifies relocations that reference symbols not defined in
|
|
// the file (neither a GLOBL/DATA symbol nor a TEXT function) and records them
|
|
// as external. The non-amd64 architectures emit relocations for every SB
|
|
// reference; this post-processing step distinguishes file-local from external.
|
|
func markExternals(img *Image, dataSyms []dataSym) {
|
|
known := make(map[string]bool, len(dataSyms)+len(img.Funcs))
|
|
for _, d := range dataSyms {
|
|
known[d.name] = true
|
|
}
|
|
for _, fn := range img.Funcs {
|
|
known[fn.Name] = true
|
|
}
|
|
externals := map[string]bool{}
|
|
for i := range img.Funcs {
|
|
for j := range img.Funcs[i].Relocs {
|
|
r := &img.Funcs[i].Relocs[j]
|
|
if !known[r.Name] {
|
|
r.External = true
|
|
externals[r.Name] = true
|
|
}
|
|
}
|
|
}
|
|
for name := range externals {
|
|
img.Externals = append(img.Externals, name)
|
|
}
|
|
sort.Strings(img.Externals)
|
|
}
|
|
|
|
// dataSym is one GLOBL symbol and its DATA initialiser.
|
|
type dataSym struct {
|
|
name string
|
|
pkg string
|
|
buf []byte
|
|
size int
|
|
static bool
|
|
rodata bool
|
|
dupok bool
|
|
}
|
|
|
|
// collectData gathers the file's static symbols (GLOBL) and their initial
|
|
// contents (DATA) into byte buffers, in declaration order.
|
|
func collectData(f *ast.File) ([]dataSym, error) {
|
|
index := map[string]int{}
|
|
var syms []dataSym
|
|
for _, d := range f.Decls {
|
|
switch dd := d.(type) {
|
|
case *ast.Globl:
|
|
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
|
continue
|
|
}
|
|
name := dd.Name.Name
|
|
if _, dup := 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)
|
|
}
|
|
index[name] = len(syms)
|
|
ds := dataSym{
|
|
name: name,
|
|
pkg: dd.Name.Pkg,
|
|
buf: make([]byte, size),
|
|
size: size,
|
|
static: dd.Name.Static,
|
|
}
|
|
for _, f := range dd.Flags {
|
|
switch f {
|
|
case "RODATA":
|
|
ds.rodata = true
|
|
case "DUPOK":
|
|
ds.dupok = true
|
|
case "1":
|
|
ds.dupok = true
|
|
case "8":
|
|
ds.rodata = true
|
|
case "9":
|
|
ds.dupok = true
|
|
ds.rodata = true
|
|
}
|
|
}
|
|
syms = append(syms, ds)
|
|
|
|
case *ast.Data:
|
|
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
|
continue
|
|
}
|
|
i, ok := index[dd.Name.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name)
|
|
}
|
|
if dd.Value == nil || !dd.Value.Imm.HasVal {
|
|
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, 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, 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 j := 0; j < w; j++ {
|
|
buf[off+int64(j)] = byte(v >> (8 * j))
|
|
}
|
|
}
|
|
}
|
|
return syms, nil
|
|
}
|
|
|
|
// align16 rounds n up to the next multiple of 16.
|
|
func align16(n int) int {
|
|
return (n + 15) &^ 15
|
|
}
|
|
|
|
// frameSize returns the local frame size declared on the TEXT directive.
|
|
func frameSize(t *ast.Text) int {
|
|
if t.Frame != nil && t.Frame.Imm.HasVal {
|
|
return int(t.Frame.Imm.Val)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// argsSize returns the argument/result area declared on the TEXT directive.
|
|
func argsSize(t *ast.Text) int {
|
|
if t.Args != nil && t.Args.Imm.HasVal {
|
|
return int(t.Args.Imm.Val)
|
|
}
|
|
return 0
|
|
}
|