Compare commits
9
Commits
96e81cc98d
...
93c47a312a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c47a312a | ||
|
|
708d0a0a5e | ||
|
|
3c8f7cb411 | ||
|
|
7c5b7a1419 | ||
|
|
bc3f448738 | ||
|
|
f37f183577 | ||
|
|
1e77e58250 | ||
|
|
1d0969ed64 | ||
|
|
23c001be51 |
@@ -79,9 +79,16 @@ jobs:
|
||||
# hanging test reports its own goroutine dump rather than a silent job kill.
|
||||
# The pattern is `packages` in the project's justfile: the logic packages, since a
|
||||
# thin cmd/ would drag the total under the floor. release.yml runs the same
|
||||
# command, so the floor is the same number everywhere.
|
||||
# command, so the floor is the same number everywhere. ./verify/... carries the
|
||||
# live oracle-parity comparison against `go tool asm` (the TestGroundTruth
|
||||
# suites); the runner's Go setup provides both the tool and GOROOT.
|
||||
run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||
|
||||
- name: Oracle parity
|
||||
# Re-run the live go-tool-asm comparison as its own step so that a parity
|
||||
# regression names the gate that failed instead of hiding inside the suite.
|
||||
run: go test -count=1 -timeout 10m -run 'TestGroundTruth' ./verify/...
|
||||
|
||||
- name: Coverage floor
|
||||
run: |
|
||||
perl -e '
|
||||
|
||||
+57
-6
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [development]
|
||||
|
||||
### Added
|
||||
|
||||
- **Indirect JMP and CALL on all four architectures.** `JMP AX`,
|
||||
`CALL AX`, `JMP (BX)` and the memory forms encode at byte parity with
|
||||
the toolchain (FF /2 and FF /4 on amd64); arm64 lowers `JMP (R0)` to
|
||||
BR and accepts the raw BR/BLR spellings; riscv64 lowers `JMP (X5)` to
|
||||
JALR; loong64 accepts the raw `JIRL rd, rj, off` spelling the Go
|
||||
assembler cannot express. A frameless amd64 function containing a
|
||||
CALL now receives the toolchain's forced base-pointer frame. The
|
||||
verify trampolines join the ground-truth lists, and a lint check for
|
||||
control flow through registers and memory extends to the new forms.
|
||||
- **`gasm asm -GOARCH` and `gasm diff -GOARCH`.** The target
|
||||
architecture can be named explicitly instead of inferred from the
|
||||
file-name suffix, which is how the suffix-less majority of GOROOT's
|
||||
`.s` files (cpu_x86.s, stub.s, ...) become assemblable.
|
||||
- **`gasm audit-instructions --corpus [dir]`.** Assembles every `.s`
|
||||
file under a directory (default GOROOT/src) with the gasm encoder
|
||||
only: suffixed files for their architecture, suffix-less files for
|
||||
all four, as a GOARCH build would. Reports the headline number (108
|
||||
of 627 GOROOT files, 17.2 %, assemble for every target architecture,
|
||||
against 23 in the previous release), the per-architecture pass rates
|
||||
and the most common failure reasons with a representative file each,
|
||||
which drive the encodability backlog by frequency.
|
||||
- **Fuzz targets for the parser and the formatter.** FuzzParse (no
|
||||
panic, always a usable file) and FuzzFormatIdempotency (formatting
|
||||
twice equals formatting once; clean input stays clean) seed
|
||||
themselves from the repository's kernels, so the plain test suite
|
||||
replays every seed in CI and `just fuzz` runs the mutation engine on
|
||||
demand.
|
||||
- **Oracle parity as its own CI step.** The push pipeline already ran
|
||||
the live go-tool-asm comparison inside the suite; a dedicated step
|
||||
now names that gate when it fails.
|
||||
- **Man pages.** docs/man carries gasm(1) and one page per command,
|
||||
written in roff: synopsis, description, every flag with its default,
|
||||
exit status, worked examples and cross-references.
|
||||
`just install-man` compresses them into ~/.local/share/man (MANDIR
|
||||
overrides) and `just uninstall-man` removes them. A test builds the
|
||||
binary and compares every command's `-h` output with its page, so the
|
||||
pages cannot drift from the CLI.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Canonical just recipes.** `just gates` is the definition of done
|
||||
@@ -52,12 +92,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The dependency statement was wrong.** `golang.org/x/arch` is not
|
||||
test-only: `gasm dis` and the debugger's listings decode through it, so
|
||||
it is linked into the binary. `CONTRIBUTING.md` and
|
||||
`docs/ARCHITECTURE.md` said otherwise.
|
||||
- **The CLI reference listed 17 of the 18 lint rules.** The missing
|
||||
`reserved-register-write` is documented with the rest.
|
||||
- **riscv64 JALR silently jumped to the wrong register.** The trampoline
|
||||
form `JALR X0, 0(X5)` read the memory operand's base as the destination,
|
||||
encoding a jump to X0 with no diagnostic; the destination is the first
|
||||
operand. The leaf detection shared the confusion, so affected functions
|
||||
also grew a bogus prologue. `JALR X0, 0(X1)` as written in the verify
|
||||
trampoline was mis-encoded since its introduction.
|
||||
- **DATA lines demanded their GLOBL first.** collectData processed the
|
||||
declarations in file order, but the Plan 9 convention puts every DATA
|
||||
line before its symbol's GLOBL; correctly ordered files (most of
|
||||
GOROOT's) failed with "no matching GLOBL". Two passes: symbols are
|
||||
registered before initialisers are applied.
|
||||
- **The formatter lost idempotency on degenerate lines.** Illegal tokens
|
||||
survived into the output, a label sharing its line with a
|
||||
non-instruction split into a line the parser rejects, stray-operand
|
||||
lines entered the alignment width computation, and rendered `/ *`,
|
||||
`> >` sequences re-lexed as comments and shifts. The label, width and
|
||||
spacing rules now agree between passes.
|
||||
|
||||
## [0.33.0] - 2026-09-14
|
||||
|
||||
|
||||
@@ -118,6 +118,11 @@ every mnemonic the real assembler accepts is recognised; what the encoder
|
||||
can emit today is narrower, and a recognised but unencodable instruction is
|
||||
reported as an explicit error, never as a wrong byte.
|
||||
|
||||
The same measurement runs over GOROOT's whole assembly corpus:
|
||||
`gasm audit-instructions --corpus` reports 108 of 627 files (17.2 %)
|
||||
assembling for every target architecture today, with the top failure
|
||||
reasons per architecture; the number moves with every release.
|
||||
|
||||
## Direction
|
||||
|
||||
The plan, in the order it is being worked:
|
||||
@@ -236,8 +241,11 @@ recipe.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/CLI.md](docs/CLI.md): full command reference
|
||||
- man pages: `just install-man` installs gasm(1) and one page per command
|
||||
into ~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
|
||||
them
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
||||
- [CHANGELOG.md](CHANGELOG.md): release history
|
||||
|
||||
|
||||
@@ -236,6 +236,11 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
|
||||
return encodeARM64BranchCond(mnem, enc.op, ops, pc, offsets, resolve)
|
||||
}
|
||||
|
||||
// Unconditional register branches (BR, BLR).
|
||||
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FUncondBranch {
|
||||
return encodeARM64RegBranch(mnem, enc.op, ops)
|
||||
}
|
||||
|
||||
// ADD/SUB immediate.
|
||||
if mnem == "ADD" || mnem == "ADDW" || mnem == "SUB" || mnem == "SUBW" ||
|
||||
mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
|
||||
@@ -337,6 +342,24 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
||||
}
|
||||
op := ops[0]
|
||||
|
||||
// Register-indirect: JMP (R0) is BR R0, CALL (R0) is BLR R0. The
|
||||
// toolchain's spelling carries no offset and no index; anything else
|
||||
// is reported rather than silently dropped.
|
||||
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
||||
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
||||
return nil, fmt.Errorf("%s: invalid indirect branch operand %q", mnem, op.Raw)
|
||||
}
|
||||
rn := arm64RegNum(op.Addr.Base)
|
||||
if rn < 0 {
|
||||
return nil, fmt.Errorf("%s: unknown branch register %q", mnem, op.Addr.Base)
|
||||
}
|
||||
opc := uint32(0) // BR
|
||||
if link {
|
||||
opc = 1 // BLR
|
||||
}
|
||||
return a64wordLE(a64UncondBranch(opc, uint32(rn), 0)), nil
|
||||
}
|
||||
|
||||
// Symbol reference: BL sym(SB), or B sym(SB) for a tail call, against a
|
||||
// relocation (R_CALLARM64 either way).
|
||||
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
||||
@@ -373,6 +396,19 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
||||
return a64wordLE(a64Branch(bop, int32(rel))), nil
|
||||
}
|
||||
|
||||
// encodeARM64RegBranch encodes BR/BLR through a register operand:
|
||||
// BR Xn = 0xd61f0000 | Rn<<5, BLR Xn = 0xd63f0000 | Rn<<5.
|
||||
func encodeARM64RegBranch(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
||||
if len(ops) != 1 {
|
||||
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||
}
|
||||
rn := arm64RegNum(operandRegName(ops[0]))
|
||||
if rn < 0 {
|
||||
return nil, fmt.Errorf("%s expects a register operand", mnem)
|
||||
}
|
||||
return a64wordLE(uint32(baseOp) | 31<<16 | uint32(rn)<<5), nil
|
||||
}
|
||||
|
||||
// encodeARM64BranchCond encodes a conditional branch (B.cond) to a label.
|
||||
func encodeARM64BranchCond(mnem string, baseOp uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
|
||||
if len(ops) != 1 {
|
||||
|
||||
@@ -572,3 +572,42 @@ func leWords(b []byte) []uint32 {
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// TestArm64IndirectBranch pins the indirect branch forms in a leaf function:
|
||||
// JMP (Rn) lowers to BR Rn, matching the toolchain's spelling, and the raw
|
||||
// BR/BLR mnemonics encode directly (a gasm superset the toolchain's front
|
||||
// end does not accept). CALL (Rn) shares the BLR path and its non-leaf
|
||||
// prologue parity is covered by the ground-truth kernel.
|
||||
func TestArm64IndirectBranch(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JMP (R0)
|
||||
BR R5
|
||||
BLR R6
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
want := []uint32{
|
||||
0xd61f0000, // BR R0
|
||||
0xd61f00a0, // BR R5
|
||||
0xd63f00c0, // BLR R6
|
||||
0xd65f03c0, // RET (BR LR)
|
||||
}
|
||||
got := leWords(img.Code)
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("word count = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-1
@@ -337,7 +337,18 @@ func computeFrame(t *ast.Text) frameInfo {
|
||||
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||
fi.size = int(t.Frame.Imm.Val)
|
||||
}
|
||||
if fi.size > 0 {
|
||||
if fi.size == 0 && hasCall(t) {
|
||||
// The toolchain gives a frameless function containing a CALL an
|
||||
// 8-byte frame for the pushed base pointer: the prologue saves BP
|
||||
// with no stack adjustment, every RET pops it back, FP references
|
||||
// pass one extra slot, and the virtual SP is the hardware SP.
|
||||
fi.size = 8
|
||||
fi.useFP = true
|
||||
fi.fpAdjust = int64(fi.size) + 16 // return address + saved BP + args base
|
||||
fi.spAdjust = 0
|
||||
fi.prologue = []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
|
||||
fi.epilogue = []byte{0x5D} // POPQ BP
|
||||
} else if fi.size > 0 {
|
||||
fi.useFP = true
|
||||
fi.fpAdjust = int64(fi.size) + 16 // frame + saved BP + return address
|
||||
fi.spAdjust = int64(fi.size)
|
||||
@@ -518,6 +529,13 @@ func instrSize(s *ast.Instr, fi frameInfo, long bool, link *linkInfo) (int, erro
|
||||
if (mnem == "CALL" || mnem == "JMP") && isSBCall(s) {
|
||||
return 5, nil // opcode + rel32, always the long form
|
||||
}
|
||||
if (mnem == "CALL" || mnem == "JMP") && indirectJumpTarget(s) {
|
||||
code, err := encodeIndirectJump(s, mnem)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(code), nil
|
||||
}
|
||||
return jumpSize(mnem, long), nil
|
||||
}
|
||||
code, _, err := encodeInstr(s, 0, nil, fi, false, nil, link)
|
||||
@@ -585,6 +603,15 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon
|
||||
}
|
||||
return append(prefix, code...), ps, nil
|
||||
}
|
||||
if (mnem == "CALL" || mnem == "JMP") && indirectJumpTarget(s) {
|
||||
// JMP/CALL through a register or memory: no relocation and no
|
||||
// label to resolve, the operand fully determines the bytes.
|
||||
code, err = encodeIndirectJump(s, mnem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return append(prefix, code...), nil, nil
|
||||
}
|
||||
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
||||
} else {
|
||||
code, ps, err = encodeNormal(s, fi, link)
|
||||
@@ -706,6 +733,44 @@ func labelName(op *ast.Operand) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// indirectJumpTarget reports whether the JMP/CALL operand addresses a
|
||||
// register or a memory location rather than a label or a static symbol.
|
||||
// A bare identifier is a register when the register table knows the name and
|
||||
// a label otherwise, which is exactly how the parser cannot distinguish them.
|
||||
func indirectJumpTarget(s *ast.Instr) bool {
|
||||
if len(s.Operands) != 1 || s.Operands[0].Kind != ast.OpAddr {
|
||||
return false
|
||||
}
|
||||
a := s.Operands[0].Addr
|
||||
if a.Base != "" || a.Index != "" {
|
||||
return true
|
||||
}
|
||||
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
|
||||
if _, ok := ParseReg(a.Sym.Name); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// encodeIndirectJump assembles a JMP/CALL through a register or memory
|
||||
// operand, which carries no relocation and no label to resolve.
|
||||
func encodeIndirectJump(s *ast.Instr, mnem string) ([]byte, error) {
|
||||
ops := make([]Operand, len(s.Operands))
|
||||
for i, op := range s.Operands {
|
||||
o, err := operandFromAST(op, 8, frameInfo{}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops[i] = o
|
||||
}
|
||||
e := &enc{}
|
||||
if err := e.encodeIndirectBranch(mnem, ops); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.out, nil
|
||||
}
|
||||
|
||||
// spReg is the hardware stack pointer used to realise FP/SP pseudo-operands.
|
||||
var spReg = Reg{idx: 4, size: 8}
|
||||
|
||||
|
||||
+14
-4
@@ -40,10 +40,20 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return e.encodeRet()
|
||||
case upper == "NOP":
|
||||
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
||||
case upper == "CALL":
|
||||
return e.encodeJmpRel(ops, []byte{0xE8})
|
||||
case upper == "JMP":
|
||||
return e.encodeJmpRel(ops, []byte{0xE9})
|
||||
case upper == "CALL" || upper == "JMP":
|
||||
// Through a register or memory: FF /2 (CALL) or FF /4 (JMP).
|
||||
// Anything else is a rel32 against a label resolved by the assembler.
|
||||
if len(ops) == 1 {
|
||||
switch ops[0].(type) {
|
||||
case Reg, Mem:
|
||||
return e.encodeIndirectBranch(upper, ops)
|
||||
}
|
||||
}
|
||||
opcode := []byte{0xE8}
|
||||
if upper == "JMP" {
|
||||
opcode = []byte{0xE9}
|
||||
}
|
||||
return e.encodeJmpRel(ops, opcode)
|
||||
}
|
||||
if cc, ok := condCode(upper); ok {
|
||||
return e.encodeJcc(cc, ops)
|
||||
|
||||
@@ -181,6 +181,39 @@ func TestControl(t *testing.T) {
|
||||
checkOp(t, x86asm.JBE, "JLS", Imm(0))
|
||||
}
|
||||
|
||||
// TestIndirectControlFlow pins the indirect JMP/CALL forms: FF /4 for JMP and
|
||||
// FF /2 for CALL through a register or memory. A REX appears only for the
|
||||
// extended registers, never REX.W: the branch operand size is fixed at 64
|
||||
// bits in long mode.
|
||||
func TestIndirectControlFlow(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"JMP AX", "JMP", []Operand{AX}, "ffe0"},
|
||||
{"CALL AX", "CALL", []Operand{AX}, "ffd0"},
|
||||
{"JMP (BX)", "JMP", []Operand{Ptr(BX, 0, 8)}, "ff23"},
|
||||
{"CALL (BX)", "CALL", []Operand{Ptr(BX, 0, 8)}, "ff13"},
|
||||
{"JMP 8(BX)", "JMP", []Operand{Ptr(BX, 8, 8)}, "ff6308"},
|
||||
{"CALL -16(BX)", "CALL", []Operand{Ptr(BX, -16, 8)}, "ff53f0"},
|
||||
{"JMP R8", "JMP", []Operand{Reg{idx: 8, size: 2}}, "41ffe0"},
|
||||
{"CALL R9", "CALL", []Operand{Reg{idx: 9, size: 2}}, "41ffd1"},
|
||||
{"JMP R15", "JMP", []Operand{Reg{idx: 15, size: 2}}, "41ffe7"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := fmt.Sprintf("%x", code); got != c.want {
|
||||
t.Errorf("%s: got %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSEMoveGroundTruth checks the legacy (non-VEX) SSE moves byte for byte
|
||||
// against the Go assembler. wantOp is the decoder's name, which differs from
|
||||
// the Plan 9 spelling for the octa moves (MOVOU = MOVDQU, MOVO = MOVDQA).
|
||||
|
||||
@@ -575,6 +575,24 @@ func (e *enc) encodeJmpRel(ops []Operand, opcode []byte) error {
|
||||
return e.emit(&instr{opcode: opcode, modrm: -1, sib: -1, imm: le32(int64(imm))})
|
||||
}
|
||||
|
||||
// encodeIndirectBranch encodes JMP/CALL through a register or memory operand:
|
||||
// FF /4 for JMP, FF /2 for CALL. The operand size is fixed at 64 bits in
|
||||
// 64-bit mode, so no REX.W is emitted; a REX appears only for R8-R15 bases.
|
||||
func (e *enc) encodeIndirectBranch(mnem string, ops []Operand) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||
}
|
||||
digit := 4 // JMP r/m64
|
||||
if mnem == "CALL" {
|
||||
digit = 2 // CALL r/m64
|
||||
}
|
||||
i := &instr{opcode: []byte{0xFF}, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, digit, ops[0], 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// condCode maps a Plan 9 conditional-jump mnemonic to its x86 condition code.
|
||||
func condCode(upper string) (int, bool) {
|
||||
if len(upper) < 2 || upper[0] != 'J' || upper == "JMP" {
|
||||
|
||||
+20
-13
@@ -440,33 +440,37 @@ type dataSym struct {
|
||||
}
|
||||
|
||||
// collectData gathers the file's static symbols (GLOBL) and their initial
|
||||
// contents (DATA) into byte buffers, in declaration order.
|
||||
// contents (DATA) into byte buffers. Two passes: the Plan 9 convention puts
|
||||
// every DATA line before its symbol's GLOBL, so the symbols are registered
|
||||
// before the initialisers are applied.
|
||||
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" {
|
||||
gd, ok := d.(*ast.Globl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := dd.Name.Name
|
||||
if gd.Name == nil || gd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
name := gd.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)
|
||||
if gd.Size != nil && gd.Size.Imm.HasVal {
|
||||
size = int(gd.Size.Imm.Val)
|
||||
}
|
||||
index[name] = len(syms)
|
||||
ds := dataSym{
|
||||
name: name,
|
||||
pkg: dd.Name.Pkg,
|
||||
pkg: gd.Name.Pkg,
|
||||
buf: make([]byte, size),
|
||||
size: size,
|
||||
static: dd.Name.Static,
|
||||
static: gd.Name.Static,
|
||||
}
|
||||
for _, f := range dd.Flags {
|
||||
for _, f := range gd.Flags {
|
||||
switch f {
|
||||
case "RODATA":
|
||||
ds.rodata = true
|
||||
@@ -487,8 +491,12 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
}
|
||||
}
|
||||
syms = append(syms, ds)
|
||||
|
||||
case *ast.Data:
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
dd, ok := d.(*ast.Data)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
@@ -518,7 +526,6 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
buf[off+int64(j)] = byte(v >> (8 * j))
|
||||
}
|
||||
}
|
||||
}
|
||||
return syms, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package asm
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
@@ -258,6 +259,9 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
|
||||
|
||||
// 16-bit branches (BEQ/BNE/BLT/BGE/BLTU/BGEU) and JIRL.
|
||||
if op, ok := l64branchTable[mnem]; ok {
|
||||
if mnem == "JIRL" {
|
||||
return encodeLOONG64Jirl(op, ops)
|
||||
}
|
||||
return encodeLOONG64Branch16(mnem, op, ops, pc, offsets, resolve)
|
||||
}
|
||||
// Single-register branches with 21-bit offsets (BLTZ/BGEZ/BLEZ/BGTZ,
|
||||
@@ -554,6 +558,46 @@ func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[stri
|
||||
return l64wordLE(l64bbl(opc, v)), nil
|
||||
}
|
||||
|
||||
// encodeLOONG64Jirl encodes the raw JIRL spelling, JIRL rd, rj, offset, the
|
||||
// form the verify trampolines use. The (rj) indirect form without an offset
|
||||
// is handled by encodeLOONG64Branch.
|
||||
func encodeLOONG64Jirl(op uint32, ops []*ast.Operand) ([]byte, error) {
|
||||
if len(ops) != 3 {
|
||||
return nil, fmt.Errorf("JIRL expects 3 operands, got %d", len(ops))
|
||||
}
|
||||
rd := l64Reg(ops[0])
|
||||
rj := l64Reg(ops[1])
|
||||
if rd < 0 || rj < 0 {
|
||||
return nil, fmt.Errorf("invalid register operand")
|
||||
}
|
||||
off, ok := l64offsetOperand(ops[2])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("JIRL expects an immediate offset, got %q", ops[2].Raw)
|
||||
}
|
||||
if (int64(off)<<16)>>16 != int64(off) {
|
||||
return nil, fmt.Errorf("JIRL offset %d out of the 16-bit range", off)
|
||||
}
|
||||
return l64wordLE(l64irr16(op, int(off), rj, rd)), nil
|
||||
}
|
||||
|
||||
// l64offsetOperand reads a bare numeric branch offset: an immediate ($n) or a
|
||||
// plain number, which parses as an empty address carrying the digits in Raw.
|
||||
func l64offsetOperand(op *ast.Operand) (int32, bool) {
|
||||
if op.Imm.HasVal {
|
||||
v := op.Imm.Val
|
||||
if op.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
return int32(v), true
|
||||
}
|
||||
if op.Kind == ast.OpAddr && op.Addr.Sym == nil && op.Addr.Base == "" && op.Addr.Index == "" {
|
||||
if v, err := strconv.ParseInt(op.Raw, 0, 64); err == nil {
|
||||
return int32(v), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// encodeLOONG64Branch16 encodes a 16-bit branch (BEQ/BNE/BLT/BGE/BLTU/BGEU):
|
||||
// INSTR rj, rd, label, or INSTR rj, label with rd = R0, which the toolchain
|
||||
// turns into the 21-bit BEQZ/BNEZ form when the register is the only operand.
|
||||
|
||||
@@ -291,3 +291,40 @@ done:
|
||||
t.Errorf("code = % x\nwant % x", code, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64IndirectBranch pins the indirect branch encodings: JMP (Rj) and
|
||||
// JAL (Rj) lower to jirl, and the raw JIRL spelling encodes the written
|
||||
// offset (the Go loong64 assembler deletes raw JIRL instructions entirely,
|
||||
// so this form is a gasm-only superset with faithful semantics).
|
||||
func TestLOONG64IndirectBranch(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JMP (R4)
|
||||
JIRL R0, R4, 8
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x4C000080, // jirl r0, r4, 0
|
||||
0x4C002080, // jirl r0, r4, 8
|
||||
0x4C000020, // jirl r0, r1, 0 (RET)
|
||||
)
|
||||
|
||||
// JAL (R5) links, so the toolchain gives the function its autosize-8
|
||||
// prologue and epilogue around the call and the closing RET.
|
||||
fn = firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JAL (R5)
|
||||
RET
|
||||
`)
|
||||
code = assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x29FFE061, // addi.d r1, r2, -8 (prologue)
|
||||
0x02FFE063, // addi.d r3, r3, -8
|
||||
0x29C00061, // st.d r1, r2, 0 (prologue saves RA)
|
||||
0x4C0000A1, // jirl r1, r5, 0
|
||||
0x28C00061, // ld.d r1, r2, 0 (epilogue restores RA)
|
||||
0x02C02063, // addi.d r3, r3, 8
|
||||
0x4C000020, // jirl r0, r1, 0 (RET)
|
||||
)
|
||||
}
|
||||
|
||||
+181
-12
@@ -135,6 +135,43 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
return out, offsets, relocs, lines, spadj, nil
|
||||
}
|
||||
|
||||
// riscvImmAlias maps the R-type ALU mnemonics onto their I-type immediate
|
||||
// forms: the toolchain accepts ADD $imm, rj, rd and emits addi. Applied
|
||||
// whenever the first operand is an immediate.
|
||||
var riscvImmAlias = map[string]string{
|
||||
"ADD": "ADDI",
|
||||
"ADDW": "ADDIW",
|
||||
"AND": "ANDI",
|
||||
"OR": "ORI",
|
||||
"XOR": "XORI",
|
||||
"SLL": "SLLI",
|
||||
"SRL": "SRLI",
|
||||
"SRA": "SRAI",
|
||||
"SLLW": "SLLIW",
|
||||
"SRLW": "SRLIW",
|
||||
"SRAW": "SRAIW",
|
||||
}
|
||||
|
||||
// riscvNormaliseImmAlias rewrites the mnemonic to its immediate form when the
|
||||
// first operand is an immediate: the toolchain accepts ADD $imm, rj, rd and
|
||||
// emits addi, and SUB $imm becomes addi with the negated immediate. The
|
||||
// second result reports that negation; the operand itself is left untouched
|
||||
// because several passes normalise the same instruction.
|
||||
func riscvNormaliseImmAlias(mnem string, ops []*ast.Operand) (string, bool) {
|
||||
if len(ops) >= 2 && isImmOperand(ops[0]) {
|
||||
switch strings.ToUpper(mnem) {
|
||||
case "SUB":
|
||||
return "ADDI", true
|
||||
case "SUBW":
|
||||
return "ADDIW", true
|
||||
}
|
||||
if alias, ok := riscvImmAlias[strings.ToUpper(mnem)]; ok {
|
||||
return alias, false
|
||||
}
|
||||
}
|
||||
return mnem, false
|
||||
}
|
||||
|
||||
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
|
||||
// Most instructions are 4 bytes; MOV with a large immediate and I-type
|
||||
// arithmetic with a large immediate expand to several (possibly compressed)
|
||||
@@ -142,10 +179,12 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||
mnem := instr.Mnemonic.Text
|
||||
ops := instr.Operands
|
||||
var immNeg bool
|
||||
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
|
||||
if mnem == "RET" {
|
||||
return len(riscvReturn(fi))
|
||||
}
|
||||
if mnem == "MOV" && len(ops) == 2 {
|
||||
if strings.HasPrefix(mnem, "MOV") && len(ops) == 2 {
|
||||
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
|
||||
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
|
||||
return 8
|
||||
@@ -173,7 +212,11 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||
}
|
||||
// I-type arithmetic with a large immediate expands to several instructions.
|
||||
if (mnem == "ADDI" || mnem == "ANDI" || mnem == "ORI" || mnem == "XORI") && len(ops) >= 1 && isImmOperand(ops[0]) {
|
||||
return riscvItypeImmediateSize(mnem, immFromOperand(ops[0]))
|
||||
imm := immFromOperand(ops[0])
|
||||
if immNeg {
|
||||
imm = -imm
|
||||
}
|
||||
return riscvItypeImmediateSize(mnem, imm)
|
||||
}
|
||||
return 4
|
||||
}
|
||||
@@ -192,6 +235,8 @@ func isBranchLike(mnem string) bool {
|
||||
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
||||
mnem := instr.Mnemonic.Text
|
||||
ops := instr.Operands
|
||||
var immNeg bool
|
||||
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
|
||||
var word uint32
|
||||
|
||||
// Handle pseudo-instructions and special cases first.
|
||||
@@ -208,6 +253,18 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
op := ops[0]
|
||||
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
|
||||
// CALL (X5): an indirect call, the toolchain's JALR X1, 0(X5).
|
||||
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
||||
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
||||
return nil, fmt.Errorf("CALL: invalid indirect operand %q", op.Raw)
|
||||
}
|
||||
rs1 := riscvRegNum(op.Addr.Base)
|
||||
if rs1 < 0 {
|
||||
return nil, fmt.Errorf("CALL: unknown branch register %q", op.Addr.Base)
|
||||
}
|
||||
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
|
||||
}
|
||||
if relocs != nil {
|
||||
@@ -229,6 +286,18 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
target = labelFromOperand(ops[0])
|
||||
// JMP (X5): an indirect branch, the toolchain's JALR X0, 0(X5).
|
||||
if ops[0].Addr.Sym == nil && ops[0].Addr.Base != "" {
|
||||
if ops[0].Addr.Offset != 0 || ops[0].Addr.Index != "" {
|
||||
return nil, fmt.Errorf("JMP: invalid indirect operand %q", ops[0].Raw)
|
||||
}
|
||||
rs1 := riscvRegNum(ops[0].Addr.Base)
|
||||
if rs1 < 0 {
|
||||
return nil, fmt.Errorf("JMP: unknown branch register %q", ops[0].Addr.Base)
|
||||
}
|
||||
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, rs1, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
}
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
@@ -255,14 +324,50 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
|
||||
// MOV is a pseudo-instruction that the Go assembler uses for loads,
|
||||
// stores, register moves and immediate loads.
|
||||
case "MOV":
|
||||
// stores, register moves and immediate loads. The width suffixes
|
||||
// (MOVB/MOVH/MOVW and unsigned forms) select the access width, and
|
||||
// MOVD/MOVF address the FP registers.
|
||||
case "MOV", "MOVB", "MOVBU", "MOVH", "MOVHU", "MOVW", "MOVWU", "MOVF", "MOVD":
|
||||
return encodeRISCVMov(instr, fi, relocs)
|
||||
|
||||
// JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1).
|
||||
case "JALR":
|
||||
return encodeRISCVJALR(instr, fi)
|
||||
|
||||
// Branch-zero pseudos: BEQZ/BNEZ compare against X0, and BLTZ/BGEZ/
|
||||
// BLEZ/BGTZ reorder the register operands of BLT/BGE accordingly.
|
||||
case "BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ":
|
||||
if len(ops) != 2 {
|
||||
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
rs := regFromOperand(ops[0])
|
||||
if rs < 0 {
|
||||
return nil, fmt.Errorf("%s: invalid register", mnem)
|
||||
}
|
||||
target := labelFromOperand(ops[1])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
var enc riscvEnc
|
||||
rs1, rs2 := rs, 0
|
||||
switch mnem {
|
||||
case "BEQZ":
|
||||
enc = riscvEnc{0x63, 0x0, 0x00} // beq rs, x0
|
||||
case "BNEZ":
|
||||
enc = riscvEnc{0x63, 0x1, 0x00} // bne rs, x0
|
||||
case "BLTZ":
|
||||
enc = riscvEnc{0x63, 0x4, 0x00} // blt rs, x0
|
||||
case "BGEZ":
|
||||
enc = riscvEnc{0x63, 0x5, 0x00} // bge rs, x0
|
||||
case "BLEZ":
|
||||
enc, rs1, rs2 = riscvEnc{0x63, 0x5, 0x00}, 0, rs // bge x0, rs
|
||||
case "BGTZ":
|
||||
enc, rs1, rs2 = riscvEnc{0x63, 0x4, 0x00}, 0, rs // blt x0, rs
|
||||
}
|
||||
word = riscvBType(enc, rs1, rs2, int32(targetOff-pc))
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
|
||||
// System instructions with no operands.
|
||||
case "FENCE", "ECALL", "EBREAK":
|
||||
enc, ok := riscvInstrTable[mnem]
|
||||
@@ -457,6 +562,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
// two-operand form INSTR $imm, rd uses rd as the source.
|
||||
case len(ops) == 3 && isITypeInstr(mnem):
|
||||
imm := immFromOperand(ops[0]) // immediate
|
||||
if immNeg {
|
||||
imm = -imm // SUB $imm arrived through the ADDI alias
|
||||
}
|
||||
rs1 := regFromOperand(ops[1]) // source register
|
||||
rd := regFromOperand(ops[2]) // destination
|
||||
if rd < 0 || rs1 < 0 {
|
||||
@@ -466,6 +574,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
|
||||
case len(ops) == 2 && isITypeInstr(mnem):
|
||||
imm := immFromOperand(ops[0])
|
||||
if immNeg {
|
||||
imm = -imm
|
||||
}
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
@@ -602,7 +713,7 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("MOV load: invalid operand")
|
||||
}
|
||||
return riscvFrameMemOp(riscvEnc{0x03, 0x3, 0x00}, false, rd, rs1, off), nil
|
||||
return riscvFrameMemOp(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), false), false, rd, rs1, off), nil
|
||||
}
|
||||
|
||||
// Register → memory (store).
|
||||
@@ -619,21 +730,70 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
||||
if rs2 < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("MOV store: invalid operand")
|
||||
}
|
||||
return riscvFrameMemOp(riscvEnc{0x23, 0x3, 0x00}, true, rs2, rs1, off), nil
|
||||
return riscvFrameMemOp(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), true), true, rs2, rs1, off), nil
|
||||
}
|
||||
|
||||
// Register → register (ADDI $0, src, dst).
|
||||
// Register → register: MOVD/MOVF are FP moves (fsgnj with rs2 = rs1),
|
||||
// everything else is ADDI $0, src, dst.
|
||||
{
|
||||
rs1 := regFromOperand(src)
|
||||
rd := regFromOperand(dst)
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("MOV: invalid register operand")
|
||||
}
|
||||
mnem := strings.ToUpper(instr.Mnemonic.Text)
|
||||
if mnem == "MOVD" || mnem == "MOVF" {
|
||||
op := uint32(0x20000053) // FSGNJ.S
|
||||
if mnem == "MOVD" {
|
||||
op = 0x22000053 // FSGNJ.D
|
||||
}
|
||||
return wordLE(op | uint32(rs1)<<15 | uint32(rs1)<<20 | uint32(rd)<<7), nil
|
||||
}
|
||||
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rs1, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// riscvMovEnc returns the load (store=false) or store (store=true) opcode for
|
||||
// a MOV-family mnemonic: the suffix selects the access width, MOVD and MOVF
|
||||
// select the FP load/store opcodes, and bare MOV is the 64-bit integer form.
|
||||
func riscvMovEnc(mnem string, store bool) riscvEnc {
|
||||
if store {
|
||||
switch mnem {
|
||||
case "MOVB":
|
||||
return riscvEnc{0x23, 0x0, 0x00} // SB
|
||||
case "MOVH":
|
||||
return riscvEnc{0x23, 0x1, 0x00} // SH
|
||||
case "MOVW":
|
||||
return riscvEnc{0x23, 0x2, 0x00} // SW
|
||||
case "MOVF":
|
||||
return riscvEnc{0x27, 0x2, 0x00} // FSW
|
||||
case "MOVD":
|
||||
return riscvEnc{0x27, 0x3, 0x00} // FSD
|
||||
}
|
||||
return riscvEnc{0x23, 0x3, 0x00} // SD
|
||||
}
|
||||
switch mnem {
|
||||
case "MOVB":
|
||||
return riscvEnc{0x03, 0x0, 0x00} // LB
|
||||
case "MOVBU":
|
||||
return riscvEnc{0x03, 0x4, 0x00} // LBU
|
||||
case "MOVH":
|
||||
return riscvEnc{0x03, 0x1, 0x00} // LH
|
||||
case "MOVHU":
|
||||
return riscvEnc{0x03, 0x5, 0x00} // LHU
|
||||
case "MOVW":
|
||||
return riscvEnc{0x03, 0x2, 0x00} // LW
|
||||
case "MOVWU":
|
||||
return riscvEnc{0x03, 0x6, 0x00} // LWU
|
||||
case "MOVF":
|
||||
return riscvEnc{0x07, 0x2, 0x00} // FLW
|
||||
case "MOVD":
|
||||
return riscvEnc{0x07, 0x3, 0x00} // FLD
|
||||
}
|
||||
return riscvEnc{0x03, 0x3, 0x00} // LD
|
||||
}
|
||||
|
||||
// riscvFrameMemOp encodes a register-relative load (store=false, I-type
|
||||
// width 0x03) or store (store=true, S-type width 0x23) of the 64-bit width
|
||||
// at off(rs1). Offsets beyond the signed 12-bit range materialise the
|
||||
@@ -886,25 +1046,34 @@ func word16(w uint16) []byte {
|
||||
}
|
||||
|
||||
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
||||
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
|
||||
// Plan 9: JALR rs1, rd (2 regs), JALR rd, offset(rs1) (the trampoline
|
||||
// form), or JALR offset(rs1) (memory → rd=X1).
|
||||
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||
ops := instr.Operands
|
||||
// JALR rd, offset(rs1): the memory operand's base is the jump-target
|
||||
// register, not the destination.
|
||||
if len(ops) == 2 && isMemOperand(ops[1]) {
|
||||
rd := regFromOperand(ops[0])
|
||||
rs1, imm := memFromOperandWithFrame(ops[1], fi)
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||
}
|
||||
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, imm)), nil
|
||||
}
|
||||
if len(ops) == 2 {
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||
}
|
||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)
|
||||
return wordLE(word), nil
|
||||
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)), nil
|
||||
}
|
||||
if len(ops) == 1 {
|
||||
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
||||
if rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid memory operand")
|
||||
}
|
||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)
|
||||
return wordLE(word), nil
|
||||
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)), nil
|
||||
}
|
||||
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
|
||||
}
|
||||
|
||||
@@ -328,6 +328,8 @@ var riscvCvtTable = map[string]riscvCvtEnc{
|
||||
"FCVTSWU": {0x68, 0x1, 0x53}, // uint32 → float32
|
||||
"FCVTSL": {0x68, 0x2, 0x53}, // int64 → float32
|
||||
"FCVTSLU": {0x68, 0x3, 0x53}, // uint64 → float32
|
||||
"FCLASSS": {0x70, 0x0, 0x53}, // classify float32 → GPR mask
|
||||
"FCLASSD": {0x70, 0x0, 0x53}, // classify float64 → GPR mask
|
||||
"FCVTDW": {0x69, 0x0, 0x53}, // int32 → float64
|
||||
"FCVTDWU": {0x69, 0x1, 0x53}, // uint32 → float64
|
||||
"FCVTDL": {0x69, 0x2, 0x53}, // int64 → float64
|
||||
|
||||
@@ -761,3 +761,24 @@ sub:
|
||||
t.Error("expected error for CALL to local label, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRISCVIndirectBranch pins the indirect branch encodings: JMP (X5) is the
|
||||
// toolchain's JALR X0, 0(X5), and the trampoline form JALR rd, offset(rs1)
|
||||
// takes its destination from the first operand (regression: the base
|
||||
// register was once read as the destination, silently jumping to X0).
|
||||
func TestRISCVIndirectBranch(t *testing.T) {
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JMP (X5)
|
||||
JALR X0, 0(X6)
|
||||
JALR X28, 0(X9)
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x00028067, // jalr x0, 5(x0), 0
|
||||
0x00030067, // jalr x0, 6(x0), 0
|
||||
0x00048e67, // jalr x28, 9(x0), 0
|
||||
0x00008067, // jalr x0, 1(x0), 0 (RET)
|
||||
)
|
||||
}
|
||||
|
||||
+10
-3
@@ -93,12 +93,19 @@ func riscvIsLeaf(t *ast.Text) bool {
|
||||
return false
|
||||
}
|
||||
case "JALR":
|
||||
// JALR rs1, rd, a call when rd is X1; JALR offset(rs1) always
|
||||
// links to X1.
|
||||
// JALR rd, offset(rs1) links when the destination register (the
|
||||
// first operand) is X1; JALR rs1, rd links when the second
|
||||
// register is X1; JALR offset(rs1) always links to X1.
|
||||
if len(in.Operands) == 1 {
|
||||
return false
|
||||
}
|
||||
if len(in.Operands) >= 2 && regFromOperand(in.Operands[1]) == 1 {
|
||||
if isMemOperand(in.Operands[1]) {
|
||||
if regFromOperand(in.Operands[0]) == 1 {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if regFromOperand(in.Operands[1]) == 1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
+206
-1
@@ -37,17 +37,29 @@ import (
|
||||
// construction and are excluded from the diff; the other architectures list
|
||||
// their conditional branches outright.
|
||||
func cmdAuditInstructions(args []string) error {
|
||||
fs := newCommand("audit-instructions", "gasm audit-instructions [amd64|arm64|riscv64|loong64]", `
|
||||
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]", `
|
||||
Compare the gasm encoder for the given architecture (default amd64) against
|
||||
go tool asm and print the diff: superset encodings (gasm-only, shippable via
|
||||
gasm asm --format goobj), known-but-unencodable names (the backlog) and go-
|
||||
only names (feature gaps). The Go side is probed black-box with a battery
|
||||
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
||||
provides.
|
||||
|
||||
With --corpus the audit changes shape: it assembles every .s file under the
|
||||
given directory (default GOROOT/src) with the gasm encoder only, no
|
||||
toolchain probing. A file whose name carries a recognisable _arch suffix is
|
||||
attempted for that architecture; a file without one is attempted for all
|
||||
four, exactly as a GOARCH build would compile it. The report gives the
|
||||
per-architecture pass rates and the most common failure reasons, which drive
|
||||
the encodability backlog by frequency rather than by table order.
|
||||
`)
|
||||
corpus := fs.Bool("corpus", false, "assemble a corpus of .s files and report pass rates and failure reasons")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *corpus {
|
||||
return cmdAuditCorpus(fs.Args())
|
||||
}
|
||||
archName := "amd64"
|
||||
switch n := len(fs.Args()); {
|
||||
case n > 1:
|
||||
@@ -305,3 +317,196 @@ func gasmAssembles(a arch.Arch, name, shape string) bool {
|
||||
func sanitize(name string) string {
|
||||
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
||||
}
|
||||
|
||||
// --- corpus audit -----------------------------------------------------------
|
||||
|
||||
// corpusTarget is one architecture row of the corpus report.
|
||||
type corpusTarget struct {
|
||||
a arch.Arch
|
||||
name string
|
||||
}
|
||||
|
||||
// corpusTally accumulates one architecture's attempts over the corpus.
|
||||
type corpusTally struct {
|
||||
attempted int
|
||||
assembled int
|
||||
reasons map[string]int // failure reason → count
|
||||
example map[string]string // failure reason → one representative file
|
||||
}
|
||||
|
||||
func (t *corpusTally) fail(path, reason string) {
|
||||
t.reasons[reason]++
|
||||
if t.example[reason] == "" {
|
||||
t.example[reason] = path
|
||||
}
|
||||
}
|
||||
|
||||
// cmdAuditCorpus implements audit-instructions --corpus.
|
||||
func cmdAuditCorpus(args []string) error {
|
||||
if len(args) > 1 {
|
||||
return fmt.Errorf("audit-instructions --corpus takes at most one directory argument")
|
||||
}
|
||||
root := ""
|
||||
if len(args) == 1 {
|
||||
root = args[0]
|
||||
} else {
|
||||
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate GOROOT: %w", err)
|
||||
}
|
||||
root = filepath.Join(strings.TrimSpace(string(out)), "src")
|
||||
}
|
||||
stats, err := runCorpusAudit(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printCorpusStats(stats)
|
||||
return nil
|
||||
}
|
||||
|
||||
// corpusStats is the outcome of one corpus audit run.
|
||||
type corpusStats struct {
|
||||
root string
|
||||
files int
|
||||
generic int // files attempted for all four architectures
|
||||
full int // files that assembled for every target architecture
|
||||
targets []corpusTarget
|
||||
tallies []*corpusTally
|
||||
}
|
||||
|
||||
// runCorpusAudit assembles every .s file under root and returns the stats.
|
||||
func runCorpusAudit(root string) (*corpusStats, error) {
|
||||
files, err := asmFiles(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targets := []corpusTarget{
|
||||
{arch.AMD64, "amd64"},
|
||||
{arch.ARM64, "arm64"},
|
||||
{arch.RISCV, "riscv64"},
|
||||
{arch.LOONG64, "loong64"},
|
||||
}
|
||||
tallies := make([]*corpusTally, len(targets))
|
||||
for i := range tallies {
|
||||
tallies[i] = &corpusTally{reasons: map[string]int{}, example: map[string]string{}}
|
||||
}
|
||||
// full is the north-star number: a file counts when every architecture
|
||||
// its name allows assembles it.
|
||||
full, generic := 0, 0
|
||||
|
||||
for _, path := range files {
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, errs := parser.Parse(path, src)
|
||||
|
||||
var wanted []int // indexes into targets
|
||||
if a := arch.FromFilename(path); a != arch.Unknown {
|
||||
for i, tg := range targets {
|
||||
if tg.a == a {
|
||||
wanted = append(wanted, i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
generic++
|
||||
for i := range targets {
|
||||
wanted = append(wanted, i)
|
||||
}
|
||||
}
|
||||
|
||||
ok := true
|
||||
for _, i := range wanted {
|
||||
tg, t := targets[i], tallies[i]
|
||||
t.attempted++
|
||||
var err error
|
||||
if len(errs) > 0 {
|
||||
err = errs[0] // a parse failure is a failure for every target
|
||||
} else {
|
||||
_, err = assembleFile(tg.a, f)
|
||||
}
|
||||
if err != nil {
|
||||
ok = false
|
||||
t.fail(path, corpusReason(err))
|
||||
continue
|
||||
}
|
||||
t.assembled++
|
||||
}
|
||||
if ok && len(wanted) > 0 {
|
||||
full++
|
||||
}
|
||||
}
|
||||
|
||||
return &corpusStats{
|
||||
root: root,
|
||||
files: len(files),
|
||||
generic: generic,
|
||||
full: full,
|
||||
targets: targets,
|
||||
tallies: tallies,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// printCorpusStats renders the corpus audit report.
|
||||
func printCorpusStats(s *corpusStats) {
|
||||
fmt.Printf("corpus %s: %d files (%d generic, attempted for all architectures)\n", s.root, s.files, s.generic)
|
||||
fmt.Printf(" assemble for every target architecture: %d (%.1f%%)\n", s.full, 100*float64(s.full)/float64(max(s.files, 1)))
|
||||
for i, tg := range s.targets {
|
||||
t := s.tallies[i]
|
||||
fmt.Printf(" %s: %d/%d attempted\n", tg.name, t.assembled, t.attempted)
|
||||
for _, r := range topReasons(t) {
|
||||
fmt.Printf(" %4d %s\n", t.reasons[r], r)
|
||||
fmt.Printf(" e.g. %s\n", t.example[r])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// corpusReason buckets an assembly or parse failure for the histogram.
|
||||
func corpusReason(err error) string {
|
||||
msg := err.Error()
|
||||
switch {
|
||||
case strings.Contains(msg, "unsupported"), strings.Contains(msg, "cannot encode"):
|
||||
return "instruction not encodable"
|
||||
case strings.Contains(msg, "undefined label"):
|
||||
return "undefined label"
|
||||
case strings.Contains(msg, "undefined symbol"), strings.Contains(msg, "external symbol"), strings.Contains(msg, "file-level assembly"):
|
||||
return "undefined symbol or external"
|
||||
case strings.Contains(msg, "operand"), strings.Contains(msg, "operand form"):
|
||||
return "unsupported operand form"
|
||||
default:
|
||||
return "other: " + firstLine(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// topReasons returns at most five reasons, most frequent first.
|
||||
func topReasons(t *corpusTally) []string {
|
||||
type kv struct {
|
||||
k string
|
||||
n int
|
||||
}
|
||||
var kvs []kv
|
||||
for k, n := range t.reasons {
|
||||
kvs = append(kvs, kv{k, n})
|
||||
}
|
||||
slices.SortFunc(kvs, func(a, b kv) int { return b.n - a.n })
|
||||
if len(kvs) > 5 {
|
||||
kvs = kvs[:5]
|
||||
}
|
||||
out := make([]string, len(kvs))
|
||||
for i, kv := range kvs {
|
||||
out[i] = kv.k
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// firstLine returns the first line of an error message, truncated.
|
||||
func firstLine(msg string) string {
|
||||
if i := strings.IndexByte(msg, '\n'); i >= 0 {
|
||||
msg = msg[:i]
|
||||
}
|
||||
if len(msg) > 80 {
|
||||
msg = msg[:80]
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
+37
-11
@@ -459,7 +459,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
|
||||
}
|
||||
|
||||
func cmdAsm(args []string) int {
|
||||
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>", `
|
||||
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-p pkg] [-GOARCH arch] [-o out] <file>", `
|
||||
Assemble FILE without the Go toolchain: every TEXT function is encoded to
|
||||
machine code and printed as a hex dump. Supported architectures: amd64
|
||||
(including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP,
|
||||
@@ -477,13 +477,22 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
out := fs.String("o", "", "write the output to this file")
|
||||
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)")
|
||||
archName := fs.String("GOARCH", "", "target architecture: amd64, arm64, riscv64 or loong64 (overrides the file-name suffix)")
|
||||
fs.Parse(args)
|
||||
if fs.NArg() != 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>")
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-GOARCH arch] [-o out] <file>")
|
||||
return 2
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
targetArch := arch.FromFilename(path)
|
||||
if *archName != "" {
|
||||
a, err := auditArch(*archName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm asm: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
targetArch = a
|
||||
}
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||
@@ -502,8 +511,10 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
||||
return 1
|
||||
}
|
||||
if len(img.Funcs) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found")
|
||||
if len(img.Funcs) == 0 && len(img.Data) == 0 {
|
||||
// A file with neither code nor data assembles to nothing, which is
|
||||
// almost always a wrong architecture rather than an intent.
|
||||
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions or GLOBL data found")
|
||||
return 1
|
||||
}
|
||||
for _, fn := range img.Funcs {
|
||||
@@ -594,7 +605,7 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
|
||||
// cmdDiff compares the machine code of two assembly files.
|
||||
func cmdDiff(args []string) int {
|
||||
set := newCommand("diff", "gasm diff <file1.s> <file2.s>", `
|
||||
set := newCommand("diff", "gasm diff [-GOARCH arch] <file1.s> <file2.s>", `
|
||||
Compare the machine code produced by assembling two files.
|
||||
Shows which functions differ and the byte-level differences.
|
||||
Useful for verifying that two implementations produce identical code,
|
||||
@@ -604,12 +615,22 @@ Use --map to compare functions whose names differ between the files,
|
||||
e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
||||
`)
|
||||
mapSpec := set.String("map", "", "comma-separated old=new pairs to match functions with different names")
|
||||
archName := set.String("GOARCH", "", "target architecture for both files: amd64, arm64, riscv64 or loong64")
|
||||
set.Parse(args)
|
||||
if set.NArg() != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm diff <file1.s> <file2.s>")
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm diff [-GOARCH arch] <file1.s> <file2.s>")
|
||||
return 2
|
||||
}
|
||||
path1, path2 := set.Arg(0), set.Arg(1)
|
||||
forced := arch.Unknown
|
||||
if *archName != "" {
|
||||
a, err := auditArch(*archName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm diff: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
forced = a
|
||||
}
|
||||
|
||||
// Parse the name mapping (file1 name → file2 name).
|
||||
nameMap := make(map[string]string)
|
||||
@@ -625,12 +646,12 @@ e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
||||
}
|
||||
|
||||
// Assemble both files.
|
||||
img1, err := assemblePath(path1)
|
||||
img1, err := assemblePath(path1, forced)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
|
||||
return 1
|
||||
}
|
||||
img2, err := assemblePath(path2)
|
||||
img2, err := assemblePath(path2, forced)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
|
||||
return 1
|
||||
@@ -705,8 +726,9 @@ func assembleFile(targetArch arch.Arch, f *ast.File) (*asm.Image, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// assemblePath reads, parses and assembles a file (used by cmdDiff).
|
||||
func assemblePath(path string) (*asm.Image, error) {
|
||||
// assemblePath reads, parses and assembles a file (used by cmdDiff). A
|
||||
// non-Unknown forced architecture overrides the file-name suffix.
|
||||
func assemblePath(path string, forced arch.Arch) (*asm.Image, error) {
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -718,7 +740,11 @@ func assemblePath(path string) (*asm.Image, error) {
|
||||
if len(errs) > 0 {
|
||||
return nil, fmt.Errorf("parse errors")
|
||||
}
|
||||
return assembleFile(arch.FromFilename(path), f)
|
||||
target := forced
|
||||
if target == arch.Unknown {
|
||||
target = arch.FromFilename(path)
|
||||
}
|
||||
return assembleFile(target, f)
|
||||
}
|
||||
|
||||
// printByteDiff shows the first few byte differences between two code blocks.
|
||||
|
||||
@@ -293,3 +293,58 @@ func TestSweepCheckLines(t *testing.T) {
|
||||
t.Errorf("sweepCheckLines = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCorpusAudit drives the corpus audit over a small fixture tree: one
|
||||
// suffixed amd64 file, one suffixed arm64 file whose body is not arm64, one
|
||||
// generic file, and one file that does not parse.
|
||||
func TestRunCorpusAudit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(name, src string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("good_amd64.s", "#include \"textflag.h\"\nTEXT ·add(SB), NOSPLIT, $0-0\n\tMOVQ AX, BX\n\tRET\n")
|
||||
write("bad_arm64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0-0\n\tMOVQ AX, BX\n\tRET\n")
|
||||
write("generic.s", "#include \"textflag.h\"\nTEXT ·g(SB), NOSPLIT, $0-0\n\tRET\n")
|
||||
write("broken.s", "#include \"textflag.h\"\nTEXT ·b(SB), NOSPLIT, $0-0\n\tJMP nowhere\n\tRET\n")
|
||||
|
||||
stats, err := runCorpusAudit(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("runCorpusAudit: %v", err)
|
||||
}
|
||||
if stats.files != 4 {
|
||||
t.Errorf("files = %d, want 4", stats.files)
|
||||
}
|
||||
if stats.generic != 2 {
|
||||
t.Errorf("generic = %d, want 2 (generic.s and broken.s)", stats.generic)
|
||||
}
|
||||
// good_amd64 and generic.s assemble everywhere they are attempted.
|
||||
if stats.full != 2 {
|
||||
t.Errorf("full = %d, want 2", stats.full)
|
||||
}
|
||||
get := func(name string) *corpusTally {
|
||||
for i, tg := range stats.targets {
|
||||
if tg.name == name {
|
||||
return stats.tallies[i]
|
||||
}
|
||||
}
|
||||
t.Fatalf("no tally for %s", name)
|
||||
return nil
|
||||
}
|
||||
// amd64: good_amd64 + generic.s + broken.s; the broken file fails to parse.
|
||||
if a := get("amd64"); a.attempted != 3 || a.assembled != 2 {
|
||||
t.Errorf("amd64 = %d/%d, want 2/3", a.assembled, a.attempted)
|
||||
}
|
||||
// arm64: bad_arm64 (MOVQ is not arm64) + generic.s + broken.s.
|
||||
if a := get("arm64"); a.attempted != 3 || a.assembled != 1 {
|
||||
t.Errorf("arm64 = %d/%d, want 1/3", a.assembled, a.attempted)
|
||||
}
|
||||
if r := get("amd64").reasons["instruction not encodable"]; r != 0 {
|
||||
t.Errorf("amd64 unexpected unencodable reason: %d", r)
|
||||
}
|
||||
if r := get("arm64").reasons["instruction not encodable"]; r != 1 {
|
||||
t.Errorf("arm64 unencodable reasons = %d, want 1", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestManPagesTrackTheCLI builds the binary once, then compares every
|
||||
// command's live `-h` output with its docs/man/gasm-<command>.1 page: the
|
||||
// flag sets must agree both ways, and the page's SYNOPSIS line must carry
|
||||
// the command's usage line. A flag or a usage change that skips the man
|
||||
// page fails here, so the pages cannot drift from the binary.
|
||||
func TestManPagesTrackTheCLI(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("builds the gasm binary")
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "gasm")
|
||||
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("build gasm: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
for _, cmd := range []string{
|
||||
"tokens", "parse", "fmt", "lint", "asm", "dis", "verify",
|
||||
"debug", "diff", "profile", "audit-instructions", "scaffold", "lsp",
|
||||
} {
|
||||
t.Run(cmd, func(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "docs", "man", "gasm-"+cmd+".1"))
|
||||
if err != nil {
|
||||
t.Fatalf("read man page: %v", err)
|
||||
}
|
||||
page := string(raw)
|
||||
|
||||
out, _ := exec.Command(bin, cmd, "-h").CombinedOutput()
|
||||
help := string(out)
|
||||
|
||||
binFlags := helpFlags(help)
|
||||
pageFlags := roffFlags(page)
|
||||
for f := range binFlags {
|
||||
if !pageFlags[f] {
|
||||
t.Errorf("flag -%s is in the binary's help but missing from the man page", f)
|
||||
}
|
||||
}
|
||||
for f := range pageFlags {
|
||||
if !binFlags[f] {
|
||||
t.Errorf("flag -%s is in the man page but the binary does not accept it", f)
|
||||
}
|
||||
}
|
||||
|
||||
want := helpUsage(help)
|
||||
got := roffSynopsis(page)
|
||||
if want != "" && got != want {
|
||||
t.Errorf("SYNOPSIS drift:\n page: %s\nbinary: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helpFlags extracts the flag names from a `gasm <cmd> -h` output.
|
||||
func helpFlags(help string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
inFlags := false
|
||||
for line := range strings.SplitSeq(help, "\n") {
|
||||
if strings.TrimRight(line, " \t") == "Flags:" {
|
||||
inFlags = true
|
||||
continue
|
||||
}
|
||||
if !inFlags {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, " -") {
|
||||
continue
|
||||
}
|
||||
token := strings.FieldsFunc(strings.TrimLeft(line, " "), func(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
})
|
||||
if len(token) == 0 {
|
||||
continue
|
||||
}
|
||||
m[strings.TrimLeft(token[0], "-")] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
var roffEscape = regexp.MustCompile(`\\f[BIRP]`)
|
||||
|
||||
// roffFlags extracts the flag names from a man page's OPTIONS section.
|
||||
func roffFlags(page string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
inOptions := false
|
||||
for line := range strings.SplitSeq(page, "\n") {
|
||||
if strings.HasPrefix(line, ".SH ") {
|
||||
inOptions = strings.HasPrefix(line, ".SH OPTIONS")
|
||||
continue
|
||||
}
|
||||
if !inOptions {
|
||||
continue
|
||||
}
|
||||
// Flag entries are written as either `.B \-flag` or `\fB\-flag`.
|
||||
var body string
|
||||
switch {
|
||||
case strings.HasPrefix(line, `.B \-`):
|
||||
body = line[3:]
|
||||
case strings.HasPrefix(line, `\fB\-`):
|
||||
body = line[1:]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
name := roffEscape.ReplaceAllString(body, "")
|
||||
name = strings.ReplaceAll(name, `\-`, "-")
|
||||
name = strings.TrimSpace(name)
|
||||
if i := strings.IndexAny(name, " \t"); i >= 0 {
|
||||
name = name[:i]
|
||||
}
|
||||
m[strings.TrimLeft(name, "-")] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// helpUsage returns the command's usage line without the "Usage: " prefix.
|
||||
func helpUsage(help string) string {
|
||||
for line := range strings.SplitSeq(help, "\n") {
|
||||
if strings.HasPrefix(line, "Usage: ") {
|
||||
return normaliseUsage(line[len("Usage: "):])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// roffSynopsis returns the page's SYNOPSIS usage line, unescaped.
|
||||
func roffSynopsis(page string) string {
|
||||
inSyn := false
|
||||
for line := range strings.SplitSeq(page, "\n") {
|
||||
if strings.HasPrefix(line, ".SH ") {
|
||||
inSyn = strings.HasPrefix(line, ".SH SYNOPSIS")
|
||||
continue
|
||||
}
|
||||
if !inSyn || !strings.HasPrefix(line, ".B ") {
|
||||
continue
|
||||
}
|
||||
return normaliseUsage(strings.ReplaceAll(line[3:], `\-`, "-"))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// normaliseUsage flattens whitespace and drops the roff font escapes so that
|
||||
// the binary's usage line and the page's SYNOPSIS line compare equal.
|
||||
func normaliseUsage(s string) string {
|
||||
s = roffEscape.ReplaceAllString(s, "")
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
@@ -161,7 +161,8 @@ Two deeper analyses sit on top of the AST:
|
||||
- **`unreachable-code`.** Code after a `RET` and before the next label is
|
||||
dead. The check is suppressed for any function whose reachability cannot be
|
||||
decided statically: those using PC-relative jumps (`JMP 2(PC)`),
|
||||
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`), or living in a
|
||||
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`, or a `JMP`/`CALL`
|
||||
through a register or memory operand), or living in a
|
||||
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator:
|
||||
code after it is occasionally intentional metadata.
|
||||
- **`register-clobber` (register liveness).** The linter builds the function's
|
||||
|
||||
+37
-5
@@ -3,6 +3,10 @@
|
||||
The reference below is taken from the program's own `--help`. If the two disagree, the
|
||||
program is right and this file is a defect.
|
||||
|
||||
The same reference is installed as man pages: `just install-man` puts gasm(1) and one
|
||||
page per command into ~/.local/share/man (`MANDIR` overrides), and a test compares each
|
||||
page against the binary so the two cannot drift apart.
|
||||
|
||||
## Synopsis
|
||||
|
||||
```sh
|
||||
@@ -134,18 +138,21 @@ gasm lint kernel_amd64.s
|
||||
## asm
|
||||
|
||||
```text
|
||||
Usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>
|
||||
Usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-GOARCH arch] [-o out] <file>
|
||||
```
|
||||
|
||||
| Flag | Default | Effect |
|
||||
|---|---|---|
|
||||
| `-format` | `raw` | output format: `raw` (concatenated image), `elf` or `goobj` (Go object) |
|
||||
| `-p` | empty | package path for `--format goobj`, qualifying the exported symbols |
|
||||
| `-GOARCH` | empty | target architecture: `amd64`, `arm64`, `riscv64` or `loong64`; overrides the file-name suffix |
|
||||
| `-o` | empty | write the output to this file instead of a hex dump on stdout |
|
||||
|
||||
Supported architectures: amd64 (VEX/AVX2 and EVEX/AVX-512 included), arm64,
|
||||
riscv64 (RV64IMAFDC and RVC) and loong64, selected from the file's `_arch.s`
|
||||
suffix. `raw` concatenates the functions and the data section into one
|
||||
riscv64 (RV64IMAFDC and RVC) and loong64, taken from the file's `_arch.s`
|
||||
suffix or from `-GOARCH`, which is how files whose names carry no
|
||||
recognisable suffix (most of GOROOT's, for example `cpu_x86.s`) are
|
||||
assembled. `raw` concatenates the functions and the data section into one
|
||||
self-consistent image; `elf` emits a relocatable object that links with the
|
||||
system toolchain; `goobj` emits the Go toolchain's own object format, which
|
||||
`cmd/link` consumes directly.
|
||||
@@ -292,11 +299,12 @@ gasm debug --func add --cover hello_amd64.s
|
||||
## diff
|
||||
|
||||
```text
|
||||
Usage: gasm diff <file1.s> <file2.s>
|
||||
Usage: gasm diff [-GOARCH arch] <file1.s> <file2.s>
|
||||
```
|
||||
|
||||
| Flag | Default | Effect |
|
||||
|---|---|---|
|
||||
| `-GOARCH` | empty | target architecture for both files, overriding the file-name suffixes |
|
||||
| `-map` | empty | comma-separated `old=new` pairs to match functions with different names |
|
||||
|
||||
Functions are paired by exact name unless `--map` says otherwise, so
|
||||
@@ -334,7 +342,7 @@ add: 16 bytes, args=24, frame=0 NOSPLIT
|
||||
## audit-instructions
|
||||
|
||||
```text
|
||||
Usage: gasm audit-instructions [amd64|arm64|riscv64|loong64]
|
||||
Usage: gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]
|
||||
```
|
||||
|
||||
Compare the gasm encoder for the given architecture (default amd64) against the
|
||||
@@ -357,6 +365,30 @@ gasm encodable: 580 go tool asm recognized: 1542
|
||||
shared: 580
|
||||
```
|
||||
|
||||
With `--corpus` the audit changes shape: it assembles every `.s` file under
|
||||
DIR (default `GOROOT/src`) with the gasm encoder only, no toolchain probing.
|
||||
A file whose name carries a recognisable `_arch` suffix is attempted for that
|
||||
architecture; a file without one is attempted for all four, exactly as a
|
||||
`GOARCH` build would compile it. The report gives the headline number (files
|
||||
that assemble for every target architecture), the per-architecture pass rates
|
||||
and the most common failure reasons with one representative file each, which
|
||||
drive the encodability backlog by frequency rather than by table order. A run
|
||||
over GOROOT takes under a second.
|
||||
|
||||
```sh
|
||||
gasm audit-instructions --corpus
|
||||
gasm audit-instructions --corpus "$(go env GOROOT)/src/crypto"
|
||||
```
|
||||
|
||||
```text
|
||||
corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures)
|
||||
assemble for every target architecture: 108 (17.2%)
|
||||
amd64: 77/464 attempted
|
||||
148 instruction not encodable
|
||||
e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386enc.s
|
||||
...
|
||||
```
|
||||
|
||||
## scaffold
|
||||
|
||||
```text
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
.TH GASM-ASM 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-asm \- assemble Plan 9 assembly without the Go toolchain
|
||||
.SH SYNOPSIS
|
||||
.B gasm asm [\-\-format raw|elf|goobj] [\-p pkg] [\-GOARCH arch] [\-o out] <file>
|
||||
.SH DESCRIPTION
|
||||
Assemble FILE without the Go toolchain: every TEXT function is encoded
|
||||
to machine code and printed as a hex dump. Supported architectures:
|
||||
amd64 (including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer,
|
||||
FP, conditional select, CRC32 and MOV pseudo), riscv64 (RV64IMAFDC and
|
||||
RVC) and loong64 (LoongArch base ISA).
|
||||
.PP
|
||||
With
|
||||
.B \-o
|
||||
the output is written to a file instead. The
|
||||
.B \-\-format
|
||||
flag selects what is written:
|
||||
.B raw
|
||||
(the default) concatenates the functions and the data section into one
|
||||
self-consistent image;
|
||||
.B elf
|
||||
emits a relocatable object (.text/.data sections, a symbol table and
|
||||
one PC32 relocation per static-symbol reference) that links with the
|
||||
system toolchain;
|
||||
.B goobj
|
||||
emits the Go toolchain's own object format, which cmd/link consumes
|
||||
directly (it requires
|
||||
.BR \-p ,
|
||||
the package path, and the installed Go toolchain).
|
||||
.PP
|
||||
Framed functions receive the stack-split guard and the trailing
|
||||
morestack block, byte-identical to the toolchain's output, so split
|
||||
functions link too.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-format \fIraw|elf|goobj\fR
|
||||
Output format; the default is raw.
|
||||
.TP
|
||||
.B \-p \fIpkg\fR
|
||||
Package path for --format goobj, qualifying the exported symbols.
|
||||
.TP
|
||||
.B \-GOARCH \fIarch\fR
|
||||
Target architecture: amd64, arm64, riscv64 or loong64; overrides the
|
||||
file-name suffix, which is how the suffix-less majority of GOROOT's
|
||||
files (cpu_x86.s, stub.s, ...) become assemblable.
|
||||
.TP
|
||||
.B \-o \fIfile\fR
|
||||
Write the output to this file instead of a hex dump on stdout.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success, 1 when parsing or assembly fails, and 2 on a usage
|
||||
error.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm asm \-o hello.bin hello_amd64.s raw image
|
||||
gasm asm \-\-format elf \-o k.o k.s linkable ELF object
|
||||
gasm asm \-\-format goobj \-p pkg/path \-o k.o k.s Go object for go build
|
||||
gasm asm \-GOARCH amd64 cpu_x86.s arch override
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-dis (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,47 @@
|
||||
.TH GASM-AUDIT-INSTRUCTIONS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-audit-instructions \- diff the encoder against the Go toolchain, or measure a corpus
|
||||
.SH SYNOPSIS
|
||||
.B gasm audit\-instructions [\-\-corpus [\fIdir\fR]] [amd64|arm64|riscv64|loong64]
|
||||
.SH DESCRIPTION
|
||||
Compare the gasm encoder for the given architecture (default amd64)
|
||||
against
|
||||
.B go tool asm
|
||||
and print the diff: superset encodings (gasm-only, shippable via
|
||||
.BR "gasm asm \-\-format goobj" ),
|
||||
known-but-unencodable names (the backlog) and go-only names (feature
|
||||
gaps). The Go side is probed black-box with a battery of bare
|
||||
mnemonics, so the audit tracks whatever toolchain
|
||||
.B go env GOROOT
|
||||
provides.
|
||||
.PP
|
||||
With
|
||||
.BR \-\-corpus ,
|
||||
the audit changes shape: it assembles every
|
||||
.I .s
|
||||
file under the given directory (default GOROOT/src) with the gasm
|
||||
encoder only, no toolchain probing. A file whose name carries a
|
||||
recognisable _arch suffix is attempted for that architecture; a file
|
||||
without one is attempted for all four, exactly as a GOARCH build would
|
||||
compile it. The report gives the headline number (files that assemble
|
||||
for every target architecture), the per-architecture pass rates and the
|
||||
most common failure reasons, which drive the encodability backlog by
|
||||
frequency rather than by table order. A run over GOROOT takes under a
|
||||
second.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-corpus [\fIdir\fR]
|
||||
Assemble a corpus of .s files and report pass rates and failure
|
||||
reasons.
|
||||
.SH EXIT STATUS
|
||||
The mnemonic-diff mode reports through its output and exits 0; a failed
|
||||
probe or an unknown architecture exits non-zero.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm audit\-instructions amd64
|
||||
gasm audit\-instructions \-\-corpus
|
||||
gasm audit\-instructions \-\-corpus "$(go env GOROOT)/src/crypto"
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,113 @@
|
||||
.TH GASM-DEBUG 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-debug \- interactive source-level debugger for JIT-assembled functions
|
||||
.SH SYNOPSIS
|
||||
.B gasm debug <file.s> \-\-func <name>
|
||||
.SH DESCRIPTION
|
||||
Interactive debugger for JIT-assembled functions. Launches the function
|
||||
in a traced subprocess (ptrace), then provides a REPL for
|
||||
single-stepping, breakpoints, register and memory inspection.
|
||||
.PP
|
||||
With
|
||||
.B \-\-script
|
||||
the REPL commands run from a file and the session ends: the headless
|
||||
mode CI and scripts use.
|
||||
.B \-\-cover
|
||||
runs to completion with a breakpoint on every instruction and reports
|
||||
which executed and how often, the label-level coverage view.
|
||||
.SH REPL COMMANDS
|
||||
.TP
|
||||
.B break \fIlabel|addr\fR [\fBif \fIreg op val\fR]
|
||||
Set a breakpoint, optionally conditional on a register comparison
|
||||
(register against register or immediate).
|
||||
.TP
|
||||
.B delete \fIlabel|addr\fR
|
||||
Remove a breakpoint.
|
||||
.TP
|
||||
.B info break
|
||||
List all breakpoints.
|
||||
.TP
|
||||
.BR step " [" n ], " s
|
||||
Single-step n instructions; the default is 1.
|
||||
.TP
|
||||
.BR next ", " n
|
||||
Step over a CALL.
|
||||
.TP
|
||||
.BR finish ", " fin
|
||||
Run until the function returns.
|
||||
.TP
|
||||
.BR continue ", " c
|
||||
Run until a breakpoint, watchpoint or exit.
|
||||
.TP
|
||||
.BR disas " [" n ], " u
|
||||
Disassemble n instructions at PC.
|
||||
.TP
|
||||
.B regs
|
||||
Print general-purpose and vector registers.
|
||||
.TP
|
||||
.B where
|
||||
Show the source line and nearest label at PC.
|
||||
.TP
|
||||
.B stack
|
||||
Show the stack near RSP (return address and ABI0 args).
|
||||
.TP
|
||||
.BR bt ", " backtrace
|
||||
Backtrace: current frame plus return address.
|
||||
.TP
|
||||
.B x [\fIaddr\fR] [\fIlen\fR]
|
||||
Hex-dump memory; the defaults are the current PC and 64 bytes.
|
||||
.TP
|
||||
.B w \fIaddr val...\fR
|
||||
Write bytes to memory.
|
||||
.TP
|
||||
.B set \fIreg value\fR
|
||||
Set a register.
|
||||
.TP
|
||||
.B watch \fIaddr\fR [\fBr|w\fR] [\fIsize\fR]
|
||||
Set a hardware watchpoint; writes are watched by default.
|
||||
.TP
|
||||
.B unwatch [\fIslot\fR]
|
||||
Clear one watchpoint, or all without an argument.
|
||||
.TP
|
||||
.BR labels ", " l
|
||||
List function labels and offsets.
|
||||
.TP
|
||||
.BR help ", " h ", " ?
|
||||
Show command help.
|
||||
.TP
|
||||
.BR quit ", " q
|
||||
Kill the debuggee and exit.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-args \fIfile\fR
|
||||
File containing the ABI0 argument block.
|
||||
.TP
|
||||
.B \-buf \fIspec\fR
|
||||
Buffer specification: name:size:pattern[,name:size:pattern...] where
|
||||
pattern is zero, ones, seq, or hex.
|
||||
.TP
|
||||
.B \-cover
|
||||
Run to completion with a breakpoint on every instruction and report
|
||||
which executed and how often.
|
||||
.TP
|
||||
.B \-func \fIname\fR
|
||||
Function to debug.
|
||||
.TP
|
||||
.B \-script \fIfile\fR
|
||||
Run REPL commands from a file (one per line) and exit; - reads stdin.
|
||||
.TP
|
||||
.B \-timeout \fIduration\fR
|
||||
Kill the debuggee after this duration (e.g. 30s); for headless --script
|
||||
runs.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when the scripted session completes and 1 when the debuggee
|
||||
crashes or a check fails; the debugger is Linux-only.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm debug \-\-func name k.s
|
||||
gasm debug \-\-func name \-\-script cmds.txt \-\-timeout 30s k.s
|
||||
gasm debug \-\-func name \-\-cover k.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,35 @@
|
||||
.TH GASM-DIFF 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-diff \- compare the machine code of two assembly files
|
||||
.SH SYNOPSIS
|
||||
.B gasm diff [\-GOARCH arch] <file1.s> <file2.s>
|
||||
.SH DESCRIPTION
|
||||
Compare the machine code produced by assembling two files. Shows which
|
||||
functions differ and the byte-level differences. Useful for verifying
|
||||
that two implementations produce identical code, or for tracking
|
||||
encoding changes between Go assembler versions.
|
||||
.PP
|
||||
Functions are paired by exact name unless
|
||||
.B \-\-map
|
||||
says otherwise, so
|
||||
.B \-\-map wideCopyAVX2=wideCopyAVX512
|
||||
pairs two variants regardless of suffix.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-GOARCH \fIarch\fR
|
||||
Target architecture for both files: amd64, arm64, riscv64 or loong64;
|
||||
overrides the file-name suffixes.
|
||||
.TP
|
||||
.B \-\-map \fIspec\fR
|
||||
Comma-separated old=new pairs to match functions with different names.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when every paired function is identical and 1 when anything
|
||||
differs; a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm diff hello_amd64.s hello_amd64.s
|
||||
gasm diff \-\-map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,36 @@
|
||||
.TH GASM-DIS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-dis \- disassemble machine code to instruction text
|
||||
.SH SYNOPSIS
|
||||
.B gasm dis [\-a arch] <file>
|
||||
.SH DESCRIPTION
|
||||
Disassemble machine code to instruction text, decoded through
|
||||
golang.org/x/arch.
|
||||
.PP
|
||||
With a
|
||||
.I .s
|
||||
file, the file is assembled first and the listing follows the real
|
||||
layout: one block per TEXT function, local labels printed at their
|
||||
offsets. The architecture comes from the file-name suffix, or from
|
||||
.BR \-a .
|
||||
.PP
|
||||
With any other file, or
|
||||
.B \-
|
||||
for standard input, the bytes are disassembled linearly and
|
||||
.B \-a
|
||||
selects the architecture (amd64, arm64, riscv64 or loong64).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-a \fIarch\fR
|
||||
Architecture for raw input: amd64, arm64, riscv64 or loong64.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success, 1 when assembly or decoding fails, and 2 on a usage
|
||||
error.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm dis k.s assemble, then list each function
|
||||
gasm dis \-a amd64 \- < dump.bin disassemble raw bytes from stdin
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,52 @@
|
||||
.TH GASM-FMT 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-fmt \- canonicalise the formatting of Plan 9 assembly sources
|
||||
.SH SYNOPSIS
|
||||
.B gasm fmt [\-w|\-l|\-d] [path...]
|
||||
.SH DESCRIPTION
|
||||
Canonicalise the formatting of Plan 9 assembly sources: indentation,
|
||||
operand spacing, per-function mnemonic alignment and blank-line layout
|
||||
(exactly one blank line before each label, TEXT and GLOBL block).
|
||||
Formatting is idempotent and preserves every line, comments included.
|
||||
.PP
|
||||
With no paths, or a directory path, every
|
||||
.I .s
|
||||
file below it is reformatted in place and the changed files are listed,
|
||||
the way
|
||||
.B go fmt
|
||||
does;
|
||||
.B .
|
||||
and
|
||||
.B _
|
||||
directories are skipped. Explicit file paths print to stdout unless
|
||||
.B \-w
|
||||
is given.
|
||||
.PP
|
||||
.B \-l
|
||||
and
|
||||
.B \-d
|
||||
rewrite nothing:
|
||||
.B \-l
|
||||
prints the paths whose formatting differs from gasm's (empty output
|
||||
means everything is formatted, which is what a CI check wants),
|
||||
.B \-d
|
||||
prints the diffs. They are mutually exclusive.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-d
|
||||
Print diffs instead of rewriting files.
|
||||
.TP
|
||||
.B \-l
|
||||
List files whose formatting differs from gasm's.
|
||||
.TP
|
||||
.B \-w
|
||||
Write the result to the source file.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm fmt reformat every .s below here
|
||||
gasm fmt \-w kernel_amd64.s canonicalise one file in place
|
||||
gasm fmt \-l *.s list files whose formatting differs
|
||||
gasm fmt \-d kernel_amd64.s print a unified diff instead
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1)
|
||||
@@ -0,0 +1,90 @@
|
||||
.TH GASM-LINT 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-lint \- run the static checks over assembly files
|
||||
.SH SYNOPSIS
|
||||
.B gasm lint <file...>
|
||||
.SH DESCRIPTION
|
||||
Run the static checks over the given files and print diagnostics as
|
||||
\fIfile:line:col: severity: message [code]\fR. The exit status is
|
||||
non-zero when an error-severity diagnostic is found; warnings (e.g. the
|
||||
register-clobber audit) do not affect it.
|
||||
.PP
|
||||
The checks are conservative: they report what can be proven wrong and
|
||||
stay quiet otherwise, so a clean lint run is meaningful without
|
||||
suppression lists.
|
||||
.SH RULES
|
||||
.TP
|
||||
.B unknown-instruction
|
||||
The mnemonic is not in the architecture's instruction table.
|
||||
.TP
|
||||
.B operand-count
|
||||
The operand count disagrees with the instruction's declared arity.
|
||||
.TP
|
||||
.B undefined-label
|
||||
A jump target names no label in the function.
|
||||
.TP
|
||||
.B duplicate-label
|
||||
Two labels in one function share a name.
|
||||
.TP
|
||||
.B missing-ret
|
||||
The function can fall off its end without a terminator.
|
||||
.TP
|
||||
.B missing-textflag-include
|
||||
TEXT flags are used without including textflag.h.
|
||||
.TP
|
||||
.B abi-argsize
|
||||
The declared frame or argument size disagrees with the
|
||||
.B //\ function
|
||||
signature.
|
||||
.TP
|
||||
.B unreachable-code
|
||||
Code after RET and before the next label is dead; suppressed for
|
||||
functions with PC-relative or register-indirect control flow.
|
||||
.TP
|
||||
.B register-clobber
|
||||
A register the Go ABI fixes across calls is written without save and
|
||||
restore, computed by liveness over the control-flow graph.
|
||||
.TP
|
||||
.B funcdata-pcdata
|
||||
FUNCDATA and PCDATA indices are malformed.
|
||||
.TP
|
||||
.B unused-label
|
||||
A label no jump reaches.
|
||||
.TP
|
||||
.B invalid-textflag
|
||||
A TEXT flag combination the toolchain rejects.
|
||||
.TP
|
||||
.B stack-imbalance
|
||||
The function does not restore the stack pointer on every path.
|
||||
.TP
|
||||
.B register-width-mismatch
|
||||
An operand register has the wrong width for the instruction.
|
||||
.TP
|
||||
.B abi0-register-args
|
||||
A call passes arguments in registers where ABI0 expects the stack
|
||||
frame.
|
||||
.TP
|
||||
.B nonportable-register-name
|
||||
A register spelling that does not exist on the target architecture.
|
||||
.TP
|
||||
.B unencodable-instruction
|
||||
The mnemonic is known to the table but the encoder cannot assemble it
|
||||
yet (amd64).
|
||||
.TP
|
||||
.B reserved-register-write
|
||||
A write to the register the runtime reserves (arm64 R18).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-disable \fIcodes\fR
|
||||
Comma-separated rule codes to disable.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when no error-severity diagnostic is found, 1 otherwise, and 2
|
||||
on a usage error.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm lint kernel_amd64.s
|
||||
gasm lint \-disable register-clobber,unused-label *.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,24 @@
|
||||
.TH GASM-LSP 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-lsp \- run the Plan 9 assembly language server
|
||||
.SH SYNOPSIS
|
||||
.B gasm lsp
|
||||
.SH DESCRIPTION
|
||||
Run the language server over standard input/output: JSON-RPC 2.0 with
|
||||
Content-Length framing. Point an LSP-capable editor at the binary and
|
||||
associate it with
|
||||
.I .s
|
||||
files; the target architecture is inferred from the file suffix
|
||||
(_amd64.s, _arm64.s, _riscv64.s, _loong64.s).
|
||||
.PP
|
||||
Provides completion, hover, document symbols, push and pull
|
||||
diagnostics, semantic-token highlighting, go-to-definition, find
|
||||
references, rename, formatting, inlay hints, code actions, signature
|
||||
help, document highlights, workspace symbol search, include document
|
||||
links and folding ranges; definition, references and rename work across
|
||||
every open document. Syntax highlighting is delivered as LSP semantic
|
||||
tokens, so no editor-specific grammar is required.
|
||||
.SH EXIT STATUS
|
||||
Runs until the client closes the session; exits 0 on a clean shutdown.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1)
|
||||
@@ -0,0 +1,20 @@
|
||||
.TH GASM-PARSE 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-parse \- parse an assembly file and report syntax errors
|
||||
.SH SYNOPSIS
|
||||
.B gasm parse <file>
|
||||
.SH DESCRIPTION
|
||||
Parse FILE and report syntax errors on stderr. The parser is
|
||||
error-tolerant and line-oriented: a malformed line becomes a diagnostic
|
||||
and parsing continues, so one run reports every syntax error in the
|
||||
file rather than the first.
|
||||
.PP
|
||||
On success, print how many declarations and TEXT functions the file
|
||||
contains. FILE may be
|
||||
.B \-
|
||||
to read standard input.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when the file parses without errors and 1 otherwise.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-tokens (1)
|
||||
@@ -0,0 +1,16 @@
|
||||
.TH GASM-PROFILE 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-profile \- show the basic-block structure of functions
|
||||
.SH SYNOPSIS
|
||||
.B gasm profile <file.s>
|
||||
.SH DESCRIPTION
|
||||
Show the basic-block structure of functions in an assembly file: each
|
||||
function's labels, their offsets, and the block boundaries. This is
|
||||
the static structure; for runtime execution counts, use
|
||||
.BR "gasm verify \-fuzz" ,
|
||||
which exercises the code paths.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success and 1 when the file cannot be assembled.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,22 @@
|
||||
.TH GASM-SCAFFOLD 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-scaffold \- generate a differential test skeleton for a kernel file
|
||||
.SH SYNOPSIS
|
||||
.B gasm scaffold differential <file.s>
|
||||
.SH DESCRIPTION
|
||||
Print a differential test skeleton for every
|
||||
.B //\ func
|
||||
signature in FILE. The test seeds random states, drives the kernel and
|
||||
a portable reference (\fI<name>Portable\fR), and compares outputs
|
||||
byte-for-byte. Write the reference bodies, place the file in the
|
||||
kernel's package, and run it in CI.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when the skeleton is written to stdout and 1 when the file
|
||||
cannot be parsed; a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm scaffold differential kernel_amd64.s > kernel_differential_test.go
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,19 @@
|
||||
.TH GASM-TOKENS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-tokens \- print the lexical token stream of an assembly file
|
||||
.SH SYNOPSIS
|
||||
.B gasm tokens <file>
|
||||
.SH DESCRIPTION
|
||||
Print the lexical token stream of FILE: position, token kind and text,
|
||||
one token per line. FILE may be
|
||||
.B \-
|
||||
to read standard input.
|
||||
.PP
|
||||
This is the front end's raw view, for when the assembler's own
|
||||
diagnostic is not enough: a mis-scanned operand or a swallowed comment
|
||||
shows up here as the tokens the parser actually received.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success and 1 when the file cannot be read.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-parse (1)
|
||||
@@ -0,0 +1,117 @@
|
||||
.TH GASM-VERIFY 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-verify \- JIT-assemble a file and run dynamic checks against it
|
||||
.SH SYNOPSIS
|
||||
.B gasm verify [\-smoke] [\-abi] [\-fuzz] [\-ground\-truth] [\-profile] [\-call] <file.s>
|
||||
.SH DESCRIPTION
|
||||
Assemble FILE, map it into executable memory and report the available
|
||||
functions. This confirms the assembled image is self-consistent (no
|
||||
unresolved external symbols) and executable, the prerequisite for
|
||||
dynamic testing.
|
||||
.PP
|
||||
With
|
||||
.BR \-smoke ,
|
||||
each NOSPLIT function is called with a zeroed argument block to confirm
|
||||
the JIT trampoline works end-to-end. This is safe only for functions
|
||||
that tolerate nil pointers and zero lengths in their arguments.
|
||||
.PP
|
||||
With
|
||||
.BR \-abi ,
|
||||
each function is called with sentinel values in the registers the Go
|
||||
ABI fixes across calls (the frame pointer and the goroutine pointer)
|
||||
plus a canary below SP; violations are reported. JIT-based checks run
|
||||
when the host matches the file's architecture (all but loong64, which
|
||||
is ground-truth only for now).
|
||||
.PP
|
||||
With
|
||||
.BR \-fuzz ,
|
||||
each function with a
|
||||
.B //\ func
|
||||
signature is differentially fuzzed against the go-tool-asm version in a
|
||||
subprocess (so a crash on a partial function is reported, not fatal).
|
||||
.PP
|
||||
With
|
||||
.BR \-ground\-truth ,
|
||||
the assembled machine code is compared byte-for-byte against
|
||||
.B go tool asm
|
||||
(relocation sites masked), reporting any encoding drift.
|
||||
.PP
|
||||
With
|
||||
.BR \-profile ,
|
||||
the static basic-block structure is listed for each function.
|
||||
.PP
|
||||
With
|
||||
.BR \-call ,
|
||||
a single function is invoked with user-supplied buffers
|
||||
.RB ( \-buf )
|
||||
instead of the smoke/abi/fuzz sweeps. Useful for partial functions
|
||||
(e.g. decoders) that crash on random input but should succeed on valid
|
||||
data.
|
||||
.PP
|
||||
With
|
||||
.B \-save\-corpus
|
||||
(and
|
||||
.BR \-fuzz ),
|
||||
every input that crashes or mismatches is written to the directory as
|
||||
replayable JSON.
|
||||
.B \-replay
|
||||
re-runs saved entries against the kernel, one child process per entry,
|
||||
so an input that crashed the original run crashes only the child: the
|
||||
report says whether each entry reproduces.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-abi
|
||||
Run ABI-checking calls (sentinel registers and red zone).
|
||||
.TP
|
||||
.B \-abi\-n \fIn\fR
|
||||
Number of ABI check iterations with varied inputs; the default is 100.
|
||||
.TP
|
||||
.B \-args \fIspec\fR
|
||||
Scalar args for -call: name=value[,name=value] (decimal or 0x hex).
|
||||
.TP
|
||||
.B \-buf \fIspec\fR
|
||||
Buffer spec for -call: name:size:pattern[,name:size:pattern] where
|
||||
pattern is zero, ones, seq, or hex.
|
||||
.TP
|
||||
.B \-call \fIname\fR
|
||||
Call a single function with -buf instead of the sweeps.
|
||||
.TP
|
||||
.B \-fuzz
|
||||
Differential fuzz: JIT both the gasm and the go-tool-asm versions and
|
||||
compare outputs.
|
||||
.TP
|
||||
.B \-ground\-truth
|
||||
Compare machine code byte-for-byte against go tool asm.
|
||||
.TP
|
||||
.B \-n \fIn\fR
|
||||
Number of fuzz iterations per function; the default is 1000.
|
||||
.TP
|
||||
.B \-profile
|
||||
List basic-block structure per function.
|
||||
.TP
|
||||
.B \-repeat \fIn\fR
|
||||
Number of times to repeat a -call invocation; the default is 1.
|
||||
.TP
|
||||
.B \-replay \fIdir\fR
|
||||
Replay saved corpus entries (JSON files in this directory) against the
|
||||
kernel.
|
||||
.TP
|
||||
.B \-save\-corpus \fIdir\fR
|
||||
With -fuzz: write each failing input to this directory as replayable
|
||||
JSON.
|
||||
.TP
|
||||
.B \-smoke
|
||||
Call each NOSPLIT function with zeroed args.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when every requested check passes and 1 when any check fails;
|
||||
a file that cannot be assembled exits 1 and a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s
|
||||
gasm verify \-\-ground\-truth k.s
|
||||
gasm verify \-\-fuzz \-n 500 k.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1),
|
||||
.BR gasm\-debug (1)
|
||||
@@ -0,0 +1,96 @@
|
||||
.TH GASM 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm \- developer tooling for Go's Plan 9 assembler
|
||||
.SH SYNOPSIS
|
||||
.B gasm
|
||||
.I command
|
||||
.RI [ arguments ]
|
||||
.br
|
||||
.B gasm
|
||||
.BR \-h | \-\-help
|
||||
.br
|
||||
.B gasm
|
||||
.BR \-V | \-\-version
|
||||
.SH DESCRIPTION
|
||||
.B gasm
|
||||
bundles a lexer, parser, formatter, linter, standalone assembler and
|
||||
language server for Plan 9 assembly into one self-contained binary. It
|
||||
serves two purposes: it brings developer tooling to the
|
||||
.I .s
|
||||
files of Go programs, and it assembles Plan 9 assembly without the Go
|
||||
toolchain at all, to raw images, linkable ELF objects with DWARF5 debug
|
||||
sections, or the Go toolchain's own GOOBJ format, which
|
||||
.B go build
|
||||
consumes directly.
|
||||
.PP
|
||||
Four architectures are covered: amd64 (including VEX/AVX2 and
|
||||
EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The
|
||||
target architecture is inferred from the file-name suffix
|
||||
(\fI_amd64.s\fR, \fI_arm64.s\fR, \fI_riscv64.s\fR, \fI_loong64.s\fR) or
|
||||
named explicitly with \fB\-GOARCH\fR where the commands accept it.
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
.B gasm\-tokens(1)
|
||||
Print the lexical token stream.
|
||||
.TP
|
||||
.B gasm\-parse(1)
|
||||
Parse a file and report syntax errors.
|
||||
.TP
|
||||
.B gasm\-fmt(1)
|
||||
Canonicalise formatting: gofmt for assembly.
|
||||
.TP
|
||||
.B gasm\-lint(1)
|
||||
Run the static checks.
|
||||
.TP
|
||||
.B gasm\-asm(1)
|
||||
Assemble \fI.s\fR files to machine code, raw images, ELF objects or GOOBJ.
|
||||
.TP
|
||||
.B gasm\-dis(1)
|
||||
Disassemble machine code, raw bytes or an assembled \fI.s\fR file.
|
||||
.TP
|
||||
.B gasm\-verify(1)
|
||||
JIT-assemble and run dynamic checks: smoke calls, ABI checks,
|
||||
differential fuzzing, ground-truth comparison.
|
||||
.TP
|
||||
.B gasm\-debug(1)
|
||||
Interactive source-level debugger.
|
||||
.TP
|
||||
.B gasm\-diff(1)
|
||||
Compare the machine code of two files byte-for-byte.
|
||||
.TP
|
||||
.B gasm\-profile(1)
|
||||
Show the basic-block structure of functions.
|
||||
.TP
|
||||
.B gasm\-audit\-instructions(1)
|
||||
Diff the encoder against the Go toolchain's name table, or measure a
|
||||
corpus of \fI.s\fR files.
|
||||
.TP
|
||||
.B gasm\-scaffold(1)
|
||||
Generate a differential test skeleton for a kernel file.
|
||||
.TP
|
||||
.B gasm\-lsp(1)
|
||||
Run the language server over standard input/output.
|
||||
.TP
|
||||
.B gasm version
|
||||
Print the version, the same as \fB\-\-version\fR.
|
||||
.SH GLOBAL FLAGS
|
||||
.TP
|
||||
.BR \-h ", " \-\-help
|
||||
Show the command overview.
|
||||
.TP
|
||||
.BR \-V ", " \-\-version
|
||||
Print the version the toolchain recorded at build time.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success, 1 when a command fails, and 2 on a usage error. An
|
||||
unknown command exits 2.
|
||||
.SH SEE ALSO
|
||||
.BR gasm\-asm (1),
|
||||
.BR gasm\-fmt (1),
|
||||
.BR gasm\-lint (1),
|
||||
.BR gasm\-verify (1),
|
||||
.BR gasm\-debug (1)
|
||||
.PP
|
||||
The full command reference, with worked examples and every flag, is in
|
||||
docs/CLI.md of the repository
|
||||
.UR https://sourcedock.dev/petrbalvin/gasm-devkit
|
||||
.UE .
|
||||
+93
-6
@@ -50,15 +50,34 @@ func Source(src string) string {
|
||||
}
|
||||
case len(line) >= 2 && line[1].Kind == token.Colon:
|
||||
inf.kind = kLabel
|
||||
// Peel stacked labels exactly as the render pass does; the
|
||||
// instruction after the last one is rendered at the
|
||||
// function's alignment width, so its mnemonic counts here.
|
||||
rest := line[2:]
|
||||
for len(rest) >= 2 && rest[0].Kind == token.Ident && rest[1].Kind == token.Colon &&
|
||||
!isDirective(rest[0].Text) {
|
||||
rest = rest[2:]
|
||||
}
|
||||
if len(rest) > 0 && rest[0].Kind == token.Ident && !isDirective(rest[0].Text) {
|
||||
inf.mnemLen = len(rest[0].Text)
|
||||
if funcID >= 0 && inf.mnemLen > maxWidth[funcID] {
|
||||
maxWidth[funcID] = inf.mnemLen
|
||||
}
|
||||
}
|
||||
default:
|
||||
inf.kind = kInstr
|
||||
inf.funcID = funcID
|
||||
// Only an identifier mnemonic takes the alignment width; a
|
||||
// line starting with anything else renders unpadded, so its
|
||||
// length must not enter the width either.
|
||||
if line[0].Kind == token.Ident {
|
||||
inf.mnemLen = len(line[0].Text)
|
||||
if funcID >= 0 && inf.mnemLen > maxWidth[funcID] {
|
||||
maxWidth[funcID] = inf.mnemLen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
infos[i] = inf
|
||||
}
|
||||
|
||||
@@ -83,12 +102,35 @@ func Source(src string) string {
|
||||
out = line[0].Text + " " + renderOps(line[1:])
|
||||
inBody = line[0].Text == "TEXT"
|
||||
case kLabel:
|
||||
out = line[0].Text + ":"
|
||||
// A label may share its line with an instruction; emit the
|
||||
// instruction on the following line.
|
||||
if rest := line[2:]; len(rest) > 0 {
|
||||
out += "\n" + renderInstr(rest, maxWidth[inf.funcID])
|
||||
// Every label, and a trailing instruction, becomes its own
|
||||
// output line: separate outLines keep the blank-line pass
|
||||
// honest about what it is looking at.
|
||||
outs = append(outs, outLine{kind: kLabel, text: line[0].Text + ":"})
|
||||
rest := line[2:]
|
||||
for len(rest) >= 2 && rest[0].Kind == token.Ident && rest[1].Kind == token.Colon &&
|
||||
!isDirective(rest[0].Text) {
|
||||
outs = append(outs, outLine{kind: kLabel, text: rest[0].Text + ":"})
|
||||
rest = rest[2:]
|
||||
}
|
||||
// A label may share its line with an instruction; the canonical
|
||||
// form puts the instruction on the following line. Trailing
|
||||
// content that does not start an instruction (a stray operand
|
||||
// token) stays on the label line: splitting it off would produce
|
||||
// a line the parser rejects.
|
||||
if len(rest) > 0 && rest[0].Kind == token.Ident && isDirective(rest[0].Text) {
|
||||
// A bare directive cannot start a line of its own (the
|
||||
// parser wants a symbol per line), so a directive sharing
|
||||
// the label's line stays there.
|
||||
outs[len(outs)-1].text += " " + strings.TrimRight(renderOps(rest), " \t")
|
||||
} else if len(rest) > 0 && rest[0].Kind == token.Ident {
|
||||
outs = append(outs, outLine{kind: kInstr, text: strings.TrimRight(renderInstr(rest, maxWidth[inf.funcID]), " \t")})
|
||||
if strings.EqualFold(rest[0].Text, "RET") {
|
||||
inBody = false
|
||||
}
|
||||
} else if len(rest) > 0 {
|
||||
outs[len(outs)-1].text += " " + renderOps(rest)
|
||||
}
|
||||
continue
|
||||
case kInstr:
|
||||
out = renderInstr(line, maxWidth[inf.funcID])
|
||||
// A RET ends the body for indentation purposes: comments that
|
||||
@@ -192,6 +234,13 @@ func renderInstr(line []token.Token, width int) string {
|
||||
if ops == "" {
|
||||
return "\t" + mnem
|
||||
}
|
||||
// Alignment is a mnemonic convention: a line that does not start with
|
||||
// an identifier (a stray operand token the parser tolerates) renders
|
||||
// unpadded, so that no alignment width can depend on it and the output
|
||||
// stays stable across passes.
|
||||
if line[0].Kind != token.Ident {
|
||||
return "\t" + mnem + " " + ops
|
||||
}
|
||||
if width < len(mnem) {
|
||||
width = len(mnem)
|
||||
}
|
||||
@@ -217,7 +266,19 @@ func renderPreproc(line []token.Token) string {
|
||||
func renderOps(toks []token.Token) string {
|
||||
var b strings.Builder
|
||||
for i, t := range toks {
|
||||
if i > 0 && spaceBetween(toks[i-1], t) {
|
||||
sp := i > 0 && spaceBetween(toks[i-1], t)
|
||||
// The accumulated text ending in '/' must never meet a '/' or '*':
|
||||
// the pair would re-lex as a comment and the next pass would see a
|
||||
// different line, whatever the token boundaries were.
|
||||
if !sp && i > 0 && (t.Kind == token.Slash || t.Kind == token.Star) && strings.HasSuffix(b.String(), "/") {
|
||||
sp = true
|
||||
}
|
||||
if sp {
|
||||
b.WriteByte(' ')
|
||||
} else if i > 0 && wouldMerge(toks[i-1], t) {
|
||||
// The tight spelling would re-lex as something else ('/'
|
||||
// before '*' opens a comment), which would make the next
|
||||
// formatting pass see a different line.
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.WriteString(t.Text)
|
||||
@@ -225,8 +286,27 @@ func renderOps(toks []token.Token) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// wouldMerge reports whether writing prev immediately before cur would
|
||||
// re-lex as something other than those two tokens: a '/' before a '*' opens
|
||||
// a comment, '>' before '>' shifts, and adjacent operators regroup.
|
||||
func wouldMerge(prev, cur token.Token) bool {
|
||||
var kinds []token.Kind
|
||||
for _, t := range lexer.Tokenize(prev.Text + cur.Text) {
|
||||
if t.Kind == token.EOF {
|
||||
break
|
||||
}
|
||||
kinds = append(kinds, t.Kind)
|
||||
}
|
||||
return len(kinds) != 2 || kinds[0] != prev.Kind || kinds[1] != cur.Kind
|
||||
}
|
||||
|
||||
// spaceBetween decides whether a single space separates prev and cur.
|
||||
func spaceBetween(prev, cur token.Token) bool {
|
||||
// '/' beside '/' or '*' would form a comment opener in the output and
|
||||
// make the next pass see a different line; keep them separated.
|
||||
if prev.Kind == token.Slash && (cur.Kind == token.Slash || cur.Kind == token.Star) {
|
||||
return true
|
||||
}
|
||||
switch cur.Kind {
|
||||
case token.RParen:
|
||||
return false
|
||||
@@ -274,6 +354,13 @@ func splitLines(toks []token.Token) [][]token.Token {
|
||||
if t.Kind == token.EOF {
|
||||
break
|
||||
}
|
||||
if t.Kind == token.Illegal {
|
||||
// Illegal tokens carry no canonical spelling: the parser
|
||||
// reports them as errors where they matter, and the formatter
|
||||
// drops them so that a stray character cannot survive into the
|
||||
// output and make the next pass render a different file.
|
||||
continue
|
||||
}
|
||||
if t.Kind == token.Newline {
|
||||
lines = append(lines, cur)
|
||||
cur = nil
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package format
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// FuzzFormatIdempotency hammers the formatter with arbitrary input. The
|
||||
// contract: formatting twice equals formatting once, and input that parses
|
||||
// cleanly still parses cleanly after formatting. The seed corpus carries the
|
||||
// repository's kernels, so a plain `go test` run replays every seed as a
|
||||
// regression case and CI exercises them without any fuzzing budget.
|
||||
func FuzzFormatIdempotency(f *testing.F) {
|
||||
for _, pattern := range []string{
|
||||
"../testdata/*.s",
|
||||
"../testdata/verify/*.s",
|
||||
} {
|
||||
files, _ := filepath.Glob(pattern)
|
||||
for _, path := range files {
|
||||
if b, err := os.ReadFile(path); err == nil {
|
||||
f.Add(string(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ AX, BX\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB),NOSPLIT,$0\n\tMOVQ AX,BX\n\n\n\tRET\n")
|
||||
f.Add("garbage ### ???\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
once := Source(src)
|
||||
twice := Source(once)
|
||||
if once != twice {
|
||||
t.Fatalf("formatting is not idempotent:\nfirst: %q\nsecond: %q", once, twice)
|
||||
}
|
||||
if _, errs := parser.Parse("in.s", src); len(errs) == 0 {
|
||||
if _, errs := parser.Parse("out.s", once); len(errs) > 0 {
|
||||
t.Fatalf("formatted output of clean input does not parse: %v\n%s", errs[0], once)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("0:A:")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("$0/ *")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("A:TEXT")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("TEXT\n0:A:A0\nA 0")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("00/ /*")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("TEXT \n0:RET\n/*0")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("A:00")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("0:A\n0:")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("0\\")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("TEXT \n\" \nA\"")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("TEXT\nA:A0000\n0A")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("0> > >>")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("0:TEXT:")
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("0:TEXT")
|
||||
@@ -13,6 +13,10 @@ packages := "./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/..
|
||||
|
||||
bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin")
|
||||
|
||||
# Where `install-man` puts the gzip-compressed pages (man1 below it). Exported because
|
||||
# the Perl recipes read it from the environment.
|
||||
export MANDIR := env_var_or_default("MANDIR", env_var("HOME") / ".local" / "share" / "man")
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
@@ -84,6 +88,37 @@ install: build
|
||||
uninstall:
|
||||
rm -f "{{bindir}}/{{binary}}"
|
||||
|
||||
# Install the man pages under docs/man into mandir/man1, gzip-compressed. Not a gate: a
|
||||
# convenience for the person at the keyboard; man finds them through ~/.local/share/man.
|
||||
install-man:
|
||||
#!/usr/bin/env perl
|
||||
my $out = $ENV{MANDIR} . q{/man1};
|
||||
system(q{mkdir}, q{-p}, $out) == 0 or die qq{mkdir $out: $!\n};
|
||||
for my $p (glob q{docs/man/*.1}) {
|
||||
open(my $g, q{-|}, q{gzip}, q{-c}, $p) or die qq{gzip $p: $!\n};
|
||||
my $content = do { local $/; <$g> };
|
||||
close($g);
|
||||
my $base = $p;
|
||||
$base =~ s{docs/man/}{};
|
||||
open(my $o, q{>}, qq{$out/$base.gz}) or die qq{write $out/$base.gz: $!\n};
|
||||
print {$o} $content;
|
||||
close($o);
|
||||
print qq{$out/$base.gz\n};
|
||||
}
|
||||
|
||||
# Remove the installed man pages.
|
||||
uninstall-man:
|
||||
#!/usr/bin/env perl
|
||||
for my $p (glob q{docs/man/*.1}) {
|
||||
my $base = $p;
|
||||
$base =~ s{docs/man/}{};
|
||||
my $f = $ENV{MANDIR} . q{/man1/} . $base . q{.gz};
|
||||
if (-f $f) {
|
||||
unlink($f) or die qq{unlink $f: $!\n};
|
||||
print qq{removed $f\n};
|
||||
}
|
||||
}
|
||||
|
||||
# Run the program. The flag is there because `go run` does not stamp the build otherwise.
|
||||
run:
|
||||
go run -buildvcs=true {{package}}
|
||||
|
||||
+26
-5
@@ -212,7 +212,7 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
||||
dead := false // inside a region unreachable from above
|
||||
reportedDead := false // the current dead region has already been reported
|
||||
hasPCRel := referencesPC(t) // PC-relative jumps defeat reachability analysis
|
||||
hasIndirect := hasIndirectBranch(t) // register-indirect branches do too
|
||||
hasIndirect := hasIndirectBranch(t, tab) // register-indirect branches do too
|
||||
// Unreachable-code analysis is only sound in functions whose control flow is
|
||||
// fully label-resolvable: no PC-relative jumps, no register-indirect
|
||||
// branches, and (file-level) no preprocessor conditionals.
|
||||
@@ -549,10 +549,12 @@ func referencesPC(t *ast.Text) bool {
|
||||
}
|
||||
|
||||
// hasIndirectBranch reports whether a function transfers control through a
|
||||
// register (JALR/JR/JIRL/BR/BLR). Such targets are computed at runtime, so
|
||||
// reachability cannot be determined statically and the unreachable-code check is
|
||||
// suppressed for the whole function.
|
||||
func hasIndirectBranch(t *ast.Text) bool {
|
||||
// register or a computed memory address: the RISC branch-register mnemonics
|
||||
// (JALR/JR/JIRL/BR/BLR), or a JMP/CALL whose target is a register or memory
|
||||
// operand rather than a label or symbol. Such targets are computed at
|
||||
// runtime, so reachability cannot be determined statically and the
|
||||
// unreachable-code check is suppressed for the whole function.
|
||||
func hasIndirectBranch(t *ast.Text, tab *arch.Table) bool {
|
||||
for _, s := range t.Body {
|
||||
in, ok := s.(*ast.Instr)
|
||||
if !ok {
|
||||
@@ -561,11 +563,30 @@ func hasIndirectBranch(t *ast.Text) bool {
|
||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||
case "JALR", "JR", "JIRL", "BR", "BLR":
|
||||
return true
|
||||
case "JMP", "CALL":
|
||||
if indirectJumpTarget(in, tab) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// indirectJumpTarget reports whether the JMP/CALL operand addresses a
|
||||
// register or a memory location rather than a label or a static symbol. The
|
||||
// parser delivers a bare register and a bare label in the same shape, so
|
||||
// register membership decides.
|
||||
func indirectJumpTarget(in *ast.Instr, tab *arch.Table) bool {
|
||||
if len(in.Operands) != 1 || in.Operands[0].Kind != ast.OpAddr {
|
||||
return false
|
||||
}
|
||||
a := in.Operands[0].Addr
|
||||
if a.Base != "" || a.Index != "" {
|
||||
return true
|
||||
}
|
||||
return a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" && tab.IsRegister(a.Sym.Name)
|
||||
}
|
||||
|
||||
// isMacroInvocation reports whether a mnemonic is a macro invocation rather
|
||||
// than a machine instruction. No Plan 9 mnemonic contains an underscore, so an
|
||||
// underscore is a reliable macro marker (the runtime headers define macros such
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
@@ -495,3 +496,33 @@ TEXT ·f(SB), NOSPLIT, $0
|
||||
t.Fatalf("amd64 must not be flagged: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasIndirectBranchShape checks that a JMP/CALL through a register or
|
||||
// memory suppresses reachability analysis, while a same-named label does not.
|
||||
func TestHasIndirectBranchShape(t *testing.T) {
|
||||
tab := arch.ForArch(arch.AMD64)
|
||||
indirect := `TEXT ·f(SB), NOSPLIT, $0
|
||||
JMP AX
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("t_amd64.s", indirect)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
if !hasIndirectBranch(f.Decls[0].(*ast.Text), tab) {
|
||||
t.Error("JMP AX: indirect branch not detected")
|
||||
}
|
||||
|
||||
label := `TEXT ·f(SB), NOSPLIT, $0
|
||||
loop:
|
||||
JMP loop
|
||||
RET
|
||||
`
|
||||
f, errs = parser.Parse("t_amd64.s", label)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
if hasIndirectBranch(f.Decls[0].(*ast.Text), tab) {
|
||||
t.Error("JMP loop: label treated as an indirect branch")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzParse hammers the parser with arbitrary input. The contract: no panic,
|
||||
// and always a usable file, whether or not diagnostics were reported. The
|
||||
// seed corpus carries the repository's kernels, so a plain `go test` run
|
||||
// replays every seed as a regression case and CI exercises them without any
|
||||
// fuzzing budget.
|
||||
func FuzzParse(f *testing.F) {
|
||||
for _, pattern := range []string{
|
||||
"../testdata/*.s",
|
||||
"../testdata/verify/*.s",
|
||||
} {
|
||||
files, _ := filepath.Glob(pattern)
|
||||
for _, path := range files {
|
||||
if b, err := os.ReadFile(path); err == nil {
|
||||
f.Add(string(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("garbage ### ??? ::: \xff\xfe\n")
|
||||
f.Add("#define A(x) x+1\nTEXT ·f(SB), $0\n\tA(2)\n\tRET\n")
|
||||
f.Add("DATA t<>+0(SB)/8, $1\nGLOBL t<>(SB), RODATA, $8\n")
|
||||
f.Add("TEXT ·f(SB), $0\n\tJMP (AX)\n\tCALL (BX)\n\tRET\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
file, _ := Parse("fuzz.s", src)
|
||||
if file == nil {
|
||||
t.Fatal("Parse returned a nil file")
|
||||
}
|
||||
})
|
||||
}
|
||||
+45
-14
@@ -438,25 +438,56 @@ func parseAddress(g []token.Token) ast.Address {
|
||||
// Optional leading displacement before a '(' base group. A sign pushes
|
||||
// the parenthesis one token further out: -4(DX) has it at i+2.
|
||||
if isSignedNumber(g, i) {
|
||||
paren := i + 1
|
||||
if g[i].Kind == token.Minus || g[i].Kind == token.Plus {
|
||||
paren = i + 2
|
||||
}
|
||||
if paren < len(g) && g[paren].Kind == token.LParen {
|
||||
j := i
|
||||
neg := false
|
||||
if g[i].Kind == token.Minus {
|
||||
if g[j].Kind == token.Minus {
|
||||
neg = true
|
||||
i++
|
||||
} else if g[i].Kind == token.Plus {
|
||||
i++
|
||||
j++
|
||||
} else if g[j].Kind == token.Plus {
|
||||
j++
|
||||
}
|
||||
if j < len(g) && g[j].Kind == token.Number {
|
||||
v := parseInt(g[j].Text)
|
||||
j++
|
||||
// A term may carry a *number factor: 0*8(base).
|
||||
for j+1 < len(g) && g[j].Kind == token.Star && g[j+1].Kind == token.Number {
|
||||
v *= parseInt(g[j+1].Text)
|
||||
j += 2
|
||||
}
|
||||
if i < len(g) && g[i].Kind == token.Number {
|
||||
addr.Offset = parseInt(g[i].Text)
|
||||
addr.HasOff = true
|
||||
if neg {
|
||||
addr.Offset = -addr.Offset
|
||||
v = -v
|
||||
}
|
||||
i++
|
||||
// Further +/- terms, each with its optional factor:
|
||||
// 3*8+8(base), 8-4*2(base).
|
||||
for {
|
||||
termNeg := false
|
||||
if j < len(g) && g[j].Kind == token.Minus {
|
||||
termNeg = true
|
||||
} else if j < len(g) && g[j].Kind == token.Plus {
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if j+1 < len(g) && g[j+1].Kind == token.Number {
|
||||
tv := parseInt(g[j+1].Text)
|
||||
j += 2
|
||||
for j+1 < len(g) && g[j].Kind == token.Star && g[j+1].Kind == token.Number {
|
||||
tv *= parseInt(g[j+1].Text)
|
||||
j += 2
|
||||
}
|
||||
if termNeg {
|
||||
tv = -tv
|
||||
}
|
||||
v += tv
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
// Commit only when the expression is followed by the base
|
||||
// group; a bare number stays untouched for the caller.
|
||||
if j < len(g) && g[j].Kind == token.LParen {
|
||||
addr.Offset = v
|
||||
addr.HasOff = true
|
||||
i = j
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Indirect control flow: JMP/CALL through a register or memory, byte-compared
|
||||
// against go tool asm. A CALL in the body also exercises the toolchain's
|
||||
// forced base-pointer frame on a frameless function.
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func f()
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
JMP AX
|
||||
CALL AX
|
||||
JMP (BX)
|
||||
CALL (BX)
|
||||
JMP 8(BX)
|
||||
JMP R8
|
||||
RET
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Indirect control flow: JMP (R0) and CALL (R0) lower to BR/BLR, the only
|
||||
// indirect-branch spellings the toolchain accepts (the raw BR/BLR mnemonics
|
||||
// stay a gasm superset).
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func f()
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
JMP (R0)
|
||||
CALL (R0)
|
||||
RET
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Indirect control flow: JMP (R4) and JAL (R5) are the toolchain's spellings
|
||||
// for jirl; the raw JIRL instruction is deliberately absent, because the Go
|
||||
// loong64 assembler deletes it and a parity kernel could not hold it.
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func f()
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JMP (R4)
|
||||
JAL (R5)
|
||||
RET
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Indirect control flow: JMP (X5) lowers to JALR X0, 0(X5), the trampoline
|
||||
// form JALR rd, offset(rs1) encodes with the destination first, and a linking
|
||||
// JALR through X1 is the toolchain's only indirect call.
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func f()
|
||||
TEXT ·leaf(SB), NOSPLIT, $0-0
|
||||
JMP (X5)
|
||||
JALR X0, 0(X6)
|
||||
RET
|
||||
|
||||
// func g()
|
||||
TEXT ·calls(SB), NOSPLIT, $0-0
|
||||
JALR X1, 0(X8)
|
||||
RET
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// GOROOT-derived shapes: the MOV width suffixes for narrow loads and the
|
||||
// branch-zero pseudos. Deliberately absent: the immediate ALU aliases
|
||||
// (AND/SUB $imm) and the FP memory forms, whose RVC compression the encoder
|
||||
// does not reproduce yet, so a parity kernel could not hold them.
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func mix(x int64, y int64) int64
|
||||
TEXT ·mix(SB), NOSPLIT, $0-24
|
||||
MOV x+0(FP), X5
|
||||
MOVWU 0(X5), X11
|
||||
MOVB 1(X5), X12
|
||||
MOVBU 2(X5), X13
|
||||
ADD X11, X12, X14
|
||||
ADD X13, X14, X15
|
||||
MOV X15, ret+16(FP)
|
||||
RET
|
||||
|
||||
// func branchy(n int64) int64
|
||||
TEXT ·branchy(SB), NOSPLIT, $0-16
|
||||
MOV n+0(FP), X5
|
||||
BEQZ X5, zero
|
||||
BNEZ X5, one
|
||||
BLTZ X5, zero
|
||||
BGEZ X5, one
|
||||
zero:
|
||||
MOV $0, X6
|
||||
MOV X6, ret+8(FP)
|
||||
RET
|
||||
one:
|
||||
MOV $1, X6
|
||||
MOV X6, ret+8(FP)
|
||||
RET
|
||||
@@ -39,7 +39,7 @@ TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
||||
MOVV 0(R5), R1 // load leaveJITCheckedRaw into RA
|
||||
MOVV R5, R3 // SP stays on the leave slot: the kernel
|
||||
// reads its first argument at SP+8
|
||||
JIRL R0, R4, 0 // jump to JIT function
|
||||
JMP (R4) // jump to JIT function
|
||||
|
||||
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
||||
// declaration, so no ABIInternal wrapper is generated; the JIT function's
|
||||
@@ -61,6 +61,6 @@ g_ok:
|
||||
MOVV savedRA(SB), R1 // restore return address
|
||||
MOVV savedG(SB), g // restore g: Go code needs it the moment it
|
||||
// resumes, violation or not
|
||||
JIRL R0, R1, 0 // return to Go caller
|
||||
JMP (R1) // return to Go caller
|
||||
|
||||
GLOBL savedG(SB), NOPTR, $8
|
||||
|
||||
@@ -25,6 +25,7 @@ func TestGroundTruthARM64(t *testing.T) {
|
||||
"../testdata/verify/call_arm64.s",
|
||||
"../testdata/verify/bigframe_arm64.s",
|
||||
"../testdata/verify/guard_arm64.s",
|
||||
"../testdata/verify/indirect_arm64.s",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
src, err := os.ReadFile(path)
|
||||
|
||||
@@ -98,6 +98,7 @@ func TestGroundTruthAMD64(t *testing.T) {
|
||||
"../testdata/verify/basic_amd64.s",
|
||||
"../testdata/verify/bigframe_amd64.s",
|
||||
"../testdata/verify/guard_amd64.s",
|
||||
"../testdata/verify/indirect_amd64.s",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
f, errs := parser.Parse(path, mustRead(t, path))
|
||||
|
||||
@@ -24,6 +24,8 @@ func TestGroundTruthLOONG64(t *testing.T) {
|
||||
"../testdata/verify/fp_loong64.s",
|
||||
"../testdata/verify/bigframe_loong64.s",
|
||||
"../testdata/verify/guard_loong64.s",
|
||||
"../testdata/verify/indirect_loong64.s",
|
||||
"trampoline_loong64.s",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
src, err := os.ReadFile(path)
|
||||
|
||||
@@ -27,6 +27,9 @@ func TestGroundTruthRISCV(t *testing.T) {
|
||||
"../testdata/verify/call_riscv64.s",
|
||||
"../testdata/verify/bigframe_riscv64.s",
|
||||
"../testdata/verify/guard_riscv64.s",
|
||||
"../testdata/verify/indirect_riscv64.s",
|
||||
"../testdata/verify/misc_riscv64.s",
|
||||
"trampoline_riscv64.s",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
testGroundTruthRISCVFile(t, path)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// ABI0 JIT trampoline for LoongArch 64.
|
||||
//
|
||||
// enterJIT saves Go SP and RA (R1), switches to the prepared stack, and
|
||||
// jumps to the JIT function. When the function RETs (JIRL zero, ra, 0),
|
||||
// jumps to the JIT function. When the function RETs (JMP (R1), the toolchain's spelling for jirl zero, ra, 0),
|
||||
// control lands in leaveJIT.
|
||||
|
||||
// func enterJIT(fn uintptr, stack uintptr)
|
||||
@@ -19,14 +19,14 @@ TEXT ·enterJIT(SB), NOSPLIT, $0-16
|
||||
MOVV R5, R3 // switch to the prepared stack: SP stays on
|
||||
// the leave slot, so the kernel reads its
|
||||
// first argument at SP+8
|
||||
JIRL R0, R4, 0 // jump to JIT function
|
||||
JMP (R4) // jump to JIT function
|
||||
|
||||
// func leaveJIT()
|
||||
TEXT ·leaveJIT(SB), NOSPLIT, $0-0
|
||||
MOVV savedSP(SB), R5 // restore Go stack pointer
|
||||
MOVV R5, R3 // restore SP
|
||||
MOVV savedRA(SB), R1 // restore return address
|
||||
JIRL R0, R1, 0 // return to Go caller
|
||||
JMP (R1) // return to Go caller
|
||||
|
||||
GLOBL savedRA(SB), NOPTR, $8
|
||||
|
||||
|
||||
Reference in New Issue
Block a user