diff --git a/disasm/arm64_naming.go b/disasm/arm64_naming.go new file mode 100644 index 0000000..5d50f1f --- /dev/null +++ b/disasm/arm64_naming.go @@ -0,0 +1,38 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package disasm + +import "fmt" + +// golang.org/x/arch refuses a corner of the arm64 exception-generation +// space outright: the hypervisor call HVC, the secure monitor call SMC and +// the speculation barrier SB. The toolchain's assembler corpus carries all +// three with machine-checked encodings (arm64enc.s's "HVC $61428 // +// 82fe1dd4" and its kin), so the disassembler names them from the raw word +// in the error branch, the way the amd64 table does. +// +// The encodings are the exception space 1101 0100 0 opc imm16 op2: HVC is +// opc 000 with the op2 marker 00010, SMC the same opc with 00011, and SB +// the fixed barrier word 0xD50330FF. Every other shape in the space keeps +// the placeholder. + +// nameARM64Rejected names an arm64 word the decoder refuses. It reports +// the rendered text, the instruction's length in bytes and whether the word +// matched a family; a non-match keeps the placeholder. +func nameARM64Rejected(code []byte) (string, int, bool) { + w := uint32(code[0]) | uint32(code[1])<<8 | uint32(code[2])<<16 | uint32(code[3])<<24 + switch { + case w == 0xd50330ff: + return "SB", 4, true + case w>>21 == 0x6a0: + imm := (w >> 5) & 0xffff + switch w & 0x1f { + case 0x02: + return fmt.Sprintf("HVC $%d", imm), 4, true + case 0x03: + return fmt.Sprintf("SMC $%d", imm), 4, true + } + } + return "", 0, false +} diff --git a/disasm/arm64_naming_test.go b/disasm/arm64_naming_test.go new file mode 100644 index 0000000..fdcac88 --- /dev/null +++ b/disasm/arm64_naming_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package disasm + +import ( + "testing" + + "sourcedock.dev/petrbalvin/gasm-sdk/arch" +) + +// TestRejectedNaming pins the supplementary arm64 naming table over the +// corpus rows whose encodings the decoder refuses: every row's bytes are +// the corpus's own expected-encoding comments (machine-checked by the +// toolchain's assembler test suite) and every text is the corpus's own +// spelling. +func TestRejectedNaming(t *testing.T) { + for _, tt := range []struct { + code []byte + text string + }{ + // arm64enc.s: HVC $61428 // 82fe1dd4 and SMC $37977 // 238b12d4. + {[]byte{0x82, 0xfe, 0x1d, 0xd4}, "HVC $61428"}, + {[]byte{0x23, 0x8b, 0x12, 0xd4}, "SMC $37977"}, + // arm64enc.s: SB // ff3003d5. + {[]byte{0xff, 0x30, 0x03, 0xd5}, "SB"}, + } { + ins, err := Decode(arch.ARM64, tt.code, 0) + if err != nil { + t.Errorf("% x: %v", tt.code, err) + continue + } + if ins.Text != tt.text || ins.Len != len(tt.code) { + t.Errorf("% x: %q (%d bytes), want %q (%d)", + tt.code, ins.Text, ins.Len, tt.text, len(tt.code)) + } + } +} + +// TestRejectedNamingBoundaries guards the table's edges: the exception +// space shapes the corpus does not spell keep the placeholder, and the +// neighbours the decoder names itself never reach the table. +func TestRejectedNamingBoundaries(t *testing.T) { + for _, tt := range []struct { + code []byte + text string + }{ + // The unallocated corners of the same opc group (op2 00000, + // 00100 and 11000), which the corpus does not spell. + {[]byte{0x00, 0x00, 0x00, 0xd4}, "???"}, + {[]byte{0x04, 0x00, 0x00, 0xd4}, "???"}, + {[]byte{0x60, 0x00, 0x00, 0xd4}, "???"}, + // BRK and HLT sit in the same space one opc step away and the + // decoder names them itself. + {[]byte{0x00, 0x00, 0x20, 0xd4}, "BRK $0"}, + {[]byte{0x00, 0x00, 0x40, 0xd4}, "HLT $0"}, + } { + ins, err := Decode(arch.ARM64, tt.code, 0) + if err != nil { + t.Errorf("% x: %v", tt.code, err) + continue + } + if ins.Text != tt.text { + t.Errorf("% x: %q, want %q", tt.code, ins.Text, tt.text) + } + } +} diff --git a/disasm/disasm.go b/disasm/disasm.go index aad4aac..0e5f3d4 100644 --- a/disasm/disasm.go +++ b/disasm/disasm.go @@ -42,6 +42,12 @@ func Decode(a arch.Arch, code []byte, addr uint64) (Instruction, error) { } 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 diff --git a/disasm/testdata/parity_arm64.txt b/disasm/testdata/parity_arm64.txt index 5f29bdd..4f32145 100644 --- a/disasm/testdata/parity_arm64.txt +++ b/disasm/testdata/parity_arm64.txt @@ -142,3 +142,6 @@ e15a8b4d VST1.P V1.H[7], (R23)(R11) b786904d VST1.P V23.D[1], (R21)(R16) 4d03631e UCVTFWD R26, F13 2c3c0e0e VMOV V1.H[3], R12 +82fe1dd4 HVC $61428 +238b12d4 SMC $37977 +ff3003d5 SB