feat(cmd): add gasm dis and share the decoder with the debugger
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package disasm decodes machine code back to instruction text for the four
|
||||
// architectures gasm assembles. It is a thin, platform-independent wrapper
|
||||
// over golang.org/x/arch and backs both the `gasm dis` command and the live
|
||||
// debugger views.
|
||||
package disasm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/arch/arm64/arm64asm"
|
||||
"golang.org/x/arch/loong64/loong64asm"
|
||||
"golang.org/x/arch/riscv64/riscv64asm"
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
)
|
||||
|
||||
// Instruction is one decoded instruction: its text form, its length in bytes
|
||||
// and the address it was decoded at.
|
||||
type Instruction struct {
|
||||
Addr uint64
|
||||
Text string
|
||||
Len int
|
||||
}
|
||||
|
||||
// Decode decodes the instruction at the start of code, located at addr.
|
||||
// code needs to hold at least the one instruction being decoded (amd64 may
|
||||
// consume up to 15 bytes). Undecodable bytes yield the placeholder text "???"
|
||||
// and a length of one word (four bytes, one on amd64) so that a listing can
|
||||
// keep making progress, mirroring the debugger's behaviour.
|
||||
func Decode(a arch.Arch, code []byte, addr uint64) (Instruction, error) {
|
||||
if len(code) == 0 {
|
||||
return Instruction{}, fmt.Errorf("disasm: empty input")
|
||||
}
|
||||
switch a {
|
||||
case arch.ARM64:
|
||||
if len(code) < 4 {
|
||||
return Instruction{}, fmt.Errorf("disasm: need 4 bytes, have %d", len(code))
|
||||
}
|
||||
inst, err := arm64asm.Decode(code)
|
||||
if err != nil {
|
||||
return Instruction{Addr: addr, Text: "???", Len: 4}, nil
|
||||
}
|
||||
return Instruction{Addr: addr, Text: arm64asm.GoSyntax(inst, addr, nil, nil), Len: 4}, nil
|
||||
|
||||
case arch.RISCV:
|
||||
// The compressed extensions are decoded transparently; a 16-bit
|
||||
// instruction only needs its two bytes.
|
||||
inst, err := riscv64asm.Decode(code)
|
||||
if err != nil {
|
||||
return Instruction{Addr: addr, Text: "???", Len: 2}, nil
|
||||
}
|
||||
return Instruction{Addr: addr, Text: riscv64asm.GoSyntax(inst, addr, nil, nil), Len: inst.Len}, nil
|
||||
|
||||
case arch.LOONG64:
|
||||
if len(code) < 4 {
|
||||
return Instruction{}, fmt.Errorf("disasm: need 4 bytes, have %d", len(code))
|
||||
}
|
||||
inst, err := loong64asm.Decode(code)
|
||||
if err != nil {
|
||||
return Instruction{Addr: addr, Text: "???", Len: 4}, nil
|
||||
}
|
||||
return Instruction{Addr: addr, Text: loong64asm.GoSyntax(inst, addr, nil), Len: 4}, nil
|
||||
|
||||
default: // amd64
|
||||
inst, err := x86asm.Decode(code, 64)
|
||||
if err != nil {
|
||||
return Instruction{Addr: addr, Text: "???", Len: 1}, nil
|
||||
}
|
||||
return Instruction{Addr: addr, Text: x86asm.IntelSyntax(inst, addr, nil), Len: inst.Len}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Block decodes up to max instructions from code starting at addr and returns
|
||||
// them in order. Decoding stops at the end of code or once an instruction
|
||||
// would run past it.
|
||||
func Block(a arch.Arch, code []byte, addr uint64, max int) []Instruction {
|
||||
var out []Instruction
|
||||
pc := 0
|
||||
for len(out) < max && pc < len(code) {
|
||||
ins, err := Decode(a, code[pc:], addr+uint64(pc))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if ins.Len <= 0 || pc+ins.Len > len(code) {
|
||||
break
|
||||
}
|
||||
out = append(out, ins)
|
||||
pc += ins.Len
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package disasm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
func TestDecodeKnownBytes(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a arch.Arch
|
||||
code []byte
|
||||
text string
|
||||
want int
|
||||
}{
|
||||
{arch.AMD64, []byte{0x55}, "push rbp", 1},
|
||||
{arch.AMD64, []byte{0x48, 0x89, 0xE5}, "mov rbp, rsp", 3},
|
||||
{arch.ARM64, []byte{0xc0, 0x03, 0x5f, 0xd6}, "RET", 4},
|
||||
{arch.RISCV, []byte{0x67, 0x80, 0x00, 0x00}, "RET", 4},
|
||||
{arch.LOONG64, []byte{0x20, 0x00, 0x00, 0x4c}, "RET", 4},
|
||||
} {
|
||||
ins, err := Decode(tt.a, tt.code, 0)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tt.a, err)
|
||||
continue
|
||||
}
|
||||
if ins.Text != tt.text || ins.Len != tt.want {
|
||||
t.Errorf("%s: % x decoded to %q (%d bytes), want %q (%d)",
|
||||
tt.a, tt.code, ins.Text, ins.Len, tt.text, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeUndecodable(t *testing.T) {
|
||||
// Zero words do not encode a usable instruction on arm64 and loong64; the
|
||||
// placeholder keeps a listing going. RISC-V is the exception: an all-zero
|
||||
// word is the defined UNIMP instruction.
|
||||
for _, a := range []arch.Arch{arch.ARM64, arch.LOONG64} {
|
||||
ins, err := Decode(a, []byte{0, 0, 0, 0}, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", a, err)
|
||||
}
|
||||
if ins.Text != "???" {
|
||||
t.Errorf("%s: text = %q, want ???", a, ins.Text)
|
||||
}
|
||||
}
|
||||
// The compressed quadrant claims the zero halfword first, so the zero
|
||||
// word decodes as the 2-byte compressed UNIMP.
|
||||
if ins, err := Decode(arch.RISCV, []byte{0, 0, 0, 0}, 0); err != nil || ins.Text != "UNIMP" || ins.Len != 2 {
|
||||
t.Errorf("riscv zero word: %q len %d err %v, want UNIMP with 2 bytes", ins.Text, ins.Len, err)
|
||||
}
|
||||
if _, err := Decode(arch.ARM64, []byte{0, 0}, 0); err == nil {
|
||||
t.Error("short input: expected an error")
|
||||
}
|
||||
if _, err := Decode(arch.AMD64, nil, 0); err == nil {
|
||||
t.Error("empty input: expected an error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlockRoundTrip assembles a small kernel with the gasm encoder for every
|
||||
// architecture and disassembles it back: the listing must cover the whole
|
||||
// function and end in RET.
|
||||
func TestBlockRoundTrip(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a arch.Arch
|
||||
name string
|
||||
}{
|
||||
{arch.AMD64, "k_amd64.s"},
|
||||
{arch.ARM64, "k_arm64.s"},
|
||||
{arch.RISCV, "k_riscv64.s"},
|
||||
{arch.LOONG64, "k_loong64.s"},
|
||||
} {
|
||||
src := "TEXT \u00b7k(SB), NOSPLIT, $0\n\tMOVQ AX, CX\n\tRET\n"
|
||||
if tt.a != arch.AMD64 {
|
||||
src = "TEXT \u00b7k(SB), NOSPLIT, $0\n\tRET\n"
|
||||
}
|
||||
f, errs := parser.Parse(tt.name, src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("%s: parse: %v", tt.a, errs)
|
||||
}
|
||||
img, err := assemble(t, tt.a, f)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: assemble: %v", tt.a, err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
|
||||
ins := Block(tt.a, code, 0, 100)
|
||||
if len(ins) == 0 {
|
||||
t.Fatalf("%s: empty listing", tt.a)
|
||||
}
|
||||
consumed := 0
|
||||
for _, in := range ins {
|
||||
if in.Text == "" || in.Text == "???" {
|
||||
t.Errorf("%s: undecoded instruction at %#x: %q", tt.a, in.Addr, in.Text)
|
||||
}
|
||||
consumed += in.Len
|
||||
}
|
||||
if consumed != len(code) {
|
||||
t.Errorf("%s: listing consumed %d of %d bytes", tt.a, consumed, len(code))
|
||||
}
|
||||
if last := ins[len(ins)-1]; !strings.Contains(strings.ToLower(last.Text), "ret") {
|
||||
t.Errorf("%s: last instruction = %q, want RET", tt.a, last.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockLimits(t *testing.T) {
|
||||
code := []byte{0x55, 0x55, 0x55, 0x55, 0x55}
|
||||
if got := Block(arch.AMD64, code, 0, 3); len(got) != 3 {
|
||||
t.Errorf("max=3 produced %d instructions, want 3", len(got))
|
||||
}
|
||||
if got := Block(arch.AMD64, code, 0, 100); len(got) != 5 {
|
||||
t.Errorf("code end produced %d instructions, want 5", len(got))
|
||||
}
|
||||
if got := Block(arch.AMD64, nil, 0, 3); len(got) != 0 {
|
||||
t.Errorf("empty code produced %d instructions, want 0", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// assemble assembles the parsed file with the encoder for a.
|
||||
func assemble(t *testing.T, a arch.Arch, f *ast.File) (*asm.Image, error) {
|
||||
t.Helper()
|
||||
switch a {
|
||||
case arch.ARM64:
|
||||
return asm.AssembleFileARM64(f)
|
||||
case arch.RISCV:
|
||||
return asm.AssembleFileRISCV(f)
|
||||
case arch.LOONG64:
|
||||
return asm.AssembleFileLOONG64(f)
|
||||
default:
|
||||
return asm.AssembleFile(f)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user