x/arch's Plan 9 renderer decodes thirty-odd scalar loong64 operations perfectly but prints them as "Unknown OP args", and names the sign-extension pair EXT.W.B/EXT.W.H "?". The supplementary naming pass re-renders them with the toolchain's own spellings: the families whose operand order x/arch already prints the Plan 9 way trade only the mnemonic, and the pointer loads and stores, the acquire loads, the release stores, PRELD and ALSL are rebuilt from the decoded arguments with the toolchain's operand order and its raw displacement reading. ADDU16I.D, a macro helper the assembler never takes as input, keeps the decoder's Unknown render. The loong64 parity fixture grows from 73 to 385 rows, pinning every unique four-byte corpus word the decoder accepts, and the LL/SC displacement divergence in the encoder is documented for asm. Assisted-by: GLM 5.3
118 lines
4.0 KiB
Go
118 lines
4.0 KiB
Go
// 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-sdk/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 {
|
|
// A few exception-space words the decoder refuses outright
|
|
// (HVC, SMC, SB) live in the supplementary naming table;
|
|
// anything else keeps the placeholder.
|
|
if text, n, ok := nameARM64Rejected(code); ok {
|
|
return Instruction{Addr: addr, Text: text, Len: n}, 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: nameLoong64(inst, loong64asm.GoSyntax(inst, addr, nil)), Len: 4}, nil
|
|
|
|
default: // amd64
|
|
inst, err := x86asm.Decode(code, 64)
|
|
if err != nil {
|
|
// A few named families the decoder refuses outright live in
|
|
// the same supplementary table; anything else keeps the
|
|
// placeholder.
|
|
if text, n, ok := nameAMD64Rejected(code); ok {
|
|
return Instruction{Addr: addr, Text: text, Len: n}, nil
|
|
}
|
|
return Instruction{Addr: addr, Text: "???", Len: 1}, nil
|
|
}
|
|
if inst.Op == 0 {
|
|
// x/arch reports a few opcode families with no error but the
|
|
// degenerate zero instruction: no opcode, no operands and a
|
|
// length of one, which GoSyntax renders as "Op(0)". The
|
|
// supplementary naming table restores the families the Go
|
|
// toolchain names; anything else keeps the placeholder.
|
|
if text, n, ok := nameAMD64Degenerate(code); ok {
|
|
return Instruction{Addr: addr, Text: text, Len: n}, nil
|
|
}
|
|
}
|
|
return Instruction{Addr: addr, Text: x86asm.GoSyntax(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
|
|
}
|