diff --git a/disasm/disasm.go b/disasm/disasm.go index 5c3da4c..8a59308 100644 --- a/disasm/disasm.go +++ b/disasm/disasm.go @@ -70,6 +70,16 @@ func Decode(a arch.Arch, code []byte, addr uint64) (Instruction, error) { if err != 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 } } diff --git a/disasm/naming_amd64.go b/disasm/naming_amd64.go new file mode 100644 index 0000000..db67981 --- /dev/null +++ b/disasm/naming_amd64.go @@ -0,0 +1,320 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package disasm + +import "fmt" + +// golang.org/x/arch decodes a handful of amd64 opcode families to no error +// and the degenerate zero instruction: no opcode, no operands, one byte. +// GoSyntax then renders the placeholder "Op(0)", decorated with whatever +// prefixes it saw, and reports a length of one. The families are the ADCX +// and ADOX carry-propagation pair, RDSEED, RDPID, the WAITPKG trio TPAUSE, +// UMONITOR and UMWAIT, and the ENDBR pair. +// +// nameAMD64Degenerate restores the names the Go toolchain itself carries +// for these families. The toolchain's assembler test corpus +// machine-checks the encodings (amd64enc.s's "ADCXL DX, DX // 660f38f6d2" +// and its kin), and the spellings below are the corpus's own; the parity +// fixtures pin every corpus row the table names, so a decoder bump that +// changes a length or a register reading fails there rather than silently +// moving the text. The table is keyed by the opcode pattern: legacy +// prefixes, opcode bytes and the ModR/M shape, so it names every encoding +// of a family, not only the corpus rows. + +// amd64GPRNames holds the Plan 9 spelling of the general-purpose registers +// by index. The names are width-independent: the mnemonic's suffix carries +// the size, so RDX reads DX in both ADCXL and ADCXQ. +var amd64GPRNames = [16]string{ + "AX", "CX", "DX", "BX", "SP", "BP", "SI", "DI", + "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15", +} + +// amd64Prefixes is the legacy prefix reading of one instruction: the +// operand-size override, the rep and repne not-really-prefixes, and REX. +// n is the number of bytes the prefixes consumed. +type amd64Prefixes struct { + osz bool // 66 + rep bool // f3 + repne bool // f2 + rexW, rexR, rexX, rexB bool + n int +} + +// scanAMD64Prefixes reads the legacy prefixes at the start of code. It +// stops at the first byte that is not one, which is where the opcode +// begins. +func scanAMD64Prefixes(code []byte) (amd64Prefixes, bool) { + var p amd64Prefixes + for ; p.n < len(code); p.n++ { + switch b := code[p.n]; { + case b == 0x66: + p.osz = true + case b == 0xf3: + p.rep = true + case b == 0xf2: + p.repne = true + case b >= 0x40 && b <= 0x4f: + p.rexW = b&0x8 != 0 + p.rexR = b&0x4 != 0 + p.rexX = b&0x2 != 0 + p.rexB = b&0x1 != 0 + default: + return p, true + } + } + return p, false // prefixes with no opcode behind them +} + +// amd64RM is the ModR/M (and SIB) reading of one operand, in the shape the +// named families use: one register or one memory reference, never an +// immediate. n counts the ModR/M byte, an SIB byte and the displacement. +type amd64RM struct { + regForm bool // mod == 11: the operand is the r/m register + reg int // reg field with REX.R applied + rm int // r/m field with REX.B applied, register form + base int // memory base register, -1 when absent + index int // memory index register, -1 when absent + scale int + disp int32 + rip bool // mod == 00, r/m == 101: RIP-relative + n int +} + +// decodeAMD64RM reads the ModR/M byte at the start of code, with the SIB +// byte and displacement that mod 00 and 01 may carry behind it. rexR, +// rexX and rexB extend the register fields to R8 through R15. +func decodeAMD64RM(code []byte, rexR, rexX, rexB bool) (amd64RM, bool) { + if len(code) == 0 { + return amd64RM{}, false + } + b := code[0] + var rm amd64RM + rm.n = 1 + rm.reg = int(b>>3) & 7 + if rexR { + rm.reg += 8 + } + mod := b >> 6 + rmb := int(b & 7) + if mod == 3 { + rm.regForm = true + rm.rm = rmb + if rexB { + rm.rm += 8 + } + return rm, true + } + rm.base, rm.index, rm.scale = -1, -1, 1 + switch rmb { + case 4: // SIB byte follows + if len(code) < rm.n+1 { + return amd64RM{}, false + } + sib := code[rm.n] + rm.n++ + rm.scale = 1 << (sib >> 6) + idx := int(sib>>3) & 7 + if rexX { + idx += 8 + } + if idx%8 != 4 { // index 100 is the no-index encoding + rm.index = idx + } + bs := int(sib & 7) + if rexB { + bs += 8 + } + if mod == 0 && bs%8 == 5 { + // base 101 with no displacement byte is disp32 alone + } else { + rm.base = bs + } + case 5: + if mod == 0 { + rm.rip = true + } else { + rm.base = rmb + if rexB { + rm.base += 8 + } + } + default: + rm.base = rmb + if rexB { + rm.base += 8 + } + } + switch mod { + case 1: + if len(code) < rm.n+1 { + return amd64RM{}, false + } + rm.disp = int32(int8(code[rm.n])) + rm.n++ + case 2: + if len(code) < rm.n+4 { + return amd64RM{}, false + } + rm.disp = int32(uint32(code[rm.n]) | uint32(code[rm.n+1])<<8 | + uint32(code[rm.n+2])<<16 | uint32(code[rm.n+3])<<24) + rm.n += 4 + } + return rm, true +} + +// text renders the operand the way the GoSyntax renderer prints a memory +// reference: the displacement in hex (a zero displacement printed as 0), +// then the base, then the scaled index. +func (rm amd64RM) text() string { + if rm.regForm { + return amd64GPRNames[rm.rm] + } + s := "0" + if rm.disp != 0 { + s = fmt.Sprintf("%#x", rm.disp) + } + if rm.rip { + // The renderer names the instruction pointer IP in a memory + // reference. + return s + "(IP)" + } + if rm.base >= 0 { + s += "(" + amd64GPRNames[rm.base] + ")" + } + if rm.index >= 0 { + s += fmt.Sprintf("(%s*%d)", amd64GPRNames[rm.index], rm.scale) + } + return s +} + +// nameAMD64Degenerate names an amd64 encoding the decoder returned as the +// degenerate zero instruction. It reports the rendered text, the +// instruction's length in bytes and whether the bytes matched a family. +// Unmatched bytes keep the renderer's own placeholder output. +func nameAMD64Degenerate(code []byte) (string, int, bool) { + p, ok := scanAMD64Prefixes(code) + if !ok || len(code) < p.n+3 || code[p.n] != 0x0f { + return "", 0, false + } + tail := code[p.n+1:] + switch tail[0] { + case 0x38: + return nameAMD64Carry(p, tail[1:]) + case 0xc7: + return nameAMD64RNG(p, tail[1:]) + case 0xae: + return nameAMD64Wait(p, tail[1:]) + case 0x1e: + return nameAMD64Endbr(p, tail[1:]) + } + return "", 0, false +} + +// nameAMD64Carry names the ADCX and ADOX pair, 0F 38 F6 /r. The operand +// size override carries ADCX and rep carries ADOX; the length suffix +// follows REX.W. The destination is the reg field and the source the +// r/m operand, printed source first. +func nameAMD64Carry(p amd64Prefixes, tail []byte) (string, int, bool) { + var name string + switch { + case p.osz && !p.rep && !p.repne: + name = "ADCX" + case p.rep && !p.osz && !p.repne: + name = "ADOX" + default: + return "", 0, false + } + if len(tail) < 2 || tail[0] != 0xf6 { + return "", 0, false + } + rm, ok := decodeAMD64RM(tail[1:], p.rexR, p.rexX, p.rexB) + if !ok { + return "", 0, false + } + suffix := "L" + if p.rexW { + suffix = "Q" + } + text := name + suffix + " " + rm.text() + ", " + amd64GPRNames[rm.reg] + return text, p.n + 3 + rm.n, true +} + +// nameAMD64RNG names RDSEED, 0F C7 /7, and its rep-prefixed sibling +// RDPID. Both take the destination register in the r/m field and exist +// only in the register form; the memory forms of the same opcode are +// CLFLUSH and its descendants, which the decoder names itself. The size +// suffix follows REX.W and the operand-size override. +func nameAMD64RNG(p amd64Prefixes, tail []byte) (string, int, bool) { + var name string + switch { + case p.rep && !p.osz && !p.repne: + name = "RDPID" + case !p.rep && !p.repne: + name = "RDSEED" + default: + return "", 0, false + } + if len(tail) < 1 { + return "", 0, false + } + rm, ok := decodeAMD64RM(tail, p.rexR, p.rexX, p.rexB) + if !ok || !rm.regForm || rm.reg != 7 { + return "", 0, false + } + if name == "RDPID" { + return name + " " + amd64GPRNames[rm.rm], p.n + 2 + rm.n, true + } + suffix := "L" + switch { + case p.rexW: + suffix = "Q" + case p.osz: + suffix = "W" + } + return name + suffix + " " + amd64GPRNames[rm.rm], p.n + 2 + rm.n, true +} + +// nameAMD64Wait names the WAITPKG and monitor trio on 0F AE /6: TPAUSE +// under the operand-size override, UMONITOR under rep and UMWAIT under +// repne. Each takes one 32-bit register in the r/m field. +func nameAMD64Wait(p amd64Prefixes, tail []byte) (string, int, bool) { + var name string + switch { + case p.osz && !p.rep && !p.repne: + name = "TPAUSE" + case p.rep && !p.osz && !p.repne: + name = "UMONITOR" + case p.repne && !p.osz && !p.rep: + name = "UMWAIT" + default: + return "", 0, false + } + if len(tail) < 1 { + return "", 0, false + } + rm, ok := decodeAMD64RM(tail, p.rexR, p.rexX, p.rexB) + if !ok || !rm.regForm || rm.reg != 6 { + return "", 0, false + } + return name + " " + amd64GPRNames[rm.rm], p.n + 2 + rm.n, true +} + +// nameAMD64Endbr names the ENDBR pair, F3 0F 1E with the fixed ModR/M +// bytes FA for the 64-bit variant and FB for the 32-bit one. The +// instructions take no operands. +func nameAMD64Endbr(p amd64Prefixes, tail []byte) (string, int, bool) { + if !p.rep || p.osz || p.repne { + return "", 0, false + } + if len(tail) < 1 { + return "", 0, false + } + switch tail[0] { + case 0xfa: + return "ENDBR64", p.n + 3, true + case 0xfb: + return "ENDBR32", p.n + 3, true + } + return "", 0, false +} diff --git a/disasm/naming_amd64_test.go b/disasm/naming_amd64_test.go new file mode 100644 index 0000000..f8f923c --- /dev/null +++ b/disasm/naming_amd64_test.go @@ -0,0 +1,128 @@ +// 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" +) + +// TestDegenerateNaming pins the supplementary naming table over the whole +// set of encodings the Go toolchain's assembler corpus carries for the +// families x/arch decodes to the degenerate zero instruction. Every row's +// bytes are the corpus's own expected-encoding comments (machine-checked +// by the toolchain's assembler test suite); every text is the corpus's own +// spelling of the instruction. +// +// The rows whose text the encoder carries are also in +// testdata/parity_amd64_unlisted.txt, where the parity and round-trip +// tests pin them; this table covers them too so a fixture edit cannot +// move one without this one noticing. ENDBR32 is the exception: the +// toolchain knows no spelling for it (its obj/x86 table carries ENDBR64 +// only), so its text is pinned here as bytes and text, with no +// round-trip. +func TestDegenerateNaming(t *testing.T) { + for _, tt := range []struct { + code []byte + text string + }{ + // amd64enc.s: ADCXL (BX), DX // 660f38f613 and kin. + {[]byte{0x66, 0x0f, 0x38, 0xf6, 0x13}, "ADCXL 0(BX), DX"}, + {[]byte{0x66, 0x41, 0x0f, 0x38, 0xf6, 0x13}, "ADCXL 0(R11), DX"}, + {[]byte{0x66, 0x0f, 0x38, 0xf6, 0xd2}, "ADCXL DX, DX"}, + {[]byte{0x66, 0x41, 0x0f, 0x38, 0xf6, 0xd3}, "ADCXL R11, DX"}, + {[]byte{0x66, 0x44, 0x0f, 0x38, 0xf6, 0x1b}, "ADCXL 0(BX), R11"}, + {[]byte{0x66, 0x45, 0x0f, 0x38, 0xf6, 0x1b}, "ADCXL 0(R11), R11"}, + {[]byte{0x66, 0x44, 0x0f, 0x38, 0xf6, 0xda}, "ADCXL DX, R11"}, + {[]byte{0x66, 0x45, 0x0f, 0x38, 0xf6, 0xdb}, "ADCXL R11, R11"}, + {[]byte{0x66, 0x48, 0x0f, 0x38, 0xf6, 0x13}, "ADCXQ 0(BX), DX"}, + {[]byte{0x66, 0x49, 0x0f, 0x38, 0xf6, 0x13}, "ADCXQ 0(R11), DX"}, + {[]byte{0x66, 0x48, 0x0f, 0x38, 0xf6, 0xd2}, "ADCXQ DX, DX"}, + {[]byte{0x66, 0x49, 0x0f, 0x38, 0xf6, 0xd3}, "ADCXQ R11, DX"}, + {[]byte{0x66, 0x4c, 0x0f, 0x38, 0xf6, 0x1b}, "ADCXQ 0(BX), R11"}, + {[]byte{0x66, 0x4d, 0x0f, 0x38, 0xf6, 0x1b}, "ADCXQ 0(R11), R11"}, + {[]byte{0x66, 0x4c, 0x0f, 0x38, 0xf6, 0xda}, "ADCXQ DX, R11"}, + {[]byte{0x66, 0x4d, 0x0f, 0x38, 0xf6, 0xdb}, "ADCXQ R11, R11"}, + // amd64enc.s: ADOXL (BX), DX // f30f38f613 and kin. + {[]byte{0xf3, 0x0f, 0x38, 0xf6, 0x13}, "ADOXL 0(BX), DX"}, + {[]byte{0xf3, 0x41, 0x0f, 0x38, 0xf6, 0x13}, "ADOXL 0(R11), DX"}, + {[]byte{0xf3, 0x0f, 0x38, 0xf6, 0xd2}, "ADOXL DX, DX"}, + {[]byte{0xf3, 0x41, 0x0f, 0x38, 0xf6, 0xd3}, "ADOXL R11, DX"}, + {[]byte{0xf3, 0x44, 0x0f, 0x38, 0xf6, 0x1b}, "ADOXL 0(BX), R11"}, + {[]byte{0xf3, 0x45, 0x0f, 0x38, 0xf6, 0x1b}, "ADOXL 0(R11), R11"}, + {[]byte{0xf3, 0x44, 0x0f, 0x38, 0xf6, 0xda}, "ADOXL DX, R11"}, + {[]byte{0xf3, 0x45, 0x0f, 0x38, 0xf6, 0xdb}, "ADOXL R11, R11"}, + {[]byte{0xf3, 0x48, 0x0f, 0x38, 0xf6, 0x13}, "ADOXQ 0(BX), DX"}, + {[]byte{0xf3, 0x49, 0x0f, 0x38, 0xf6, 0x13}, "ADOXQ 0(R11), DX"}, + {[]byte{0xf3, 0x48, 0x0f, 0x38, 0xf6, 0xd2}, "ADOXQ DX, DX"}, + {[]byte{0xf3, 0x49, 0x0f, 0x38, 0xf6, 0xd3}, "ADOXQ R11, DX"}, + {[]byte{0xf3, 0x4c, 0x0f, 0x38, 0xf6, 0x1b}, "ADOXQ 0(BX), R11"}, + {[]byte{0xf3, 0x4d, 0x0f, 0x38, 0xf6, 0x1b}, "ADOXQ 0(R11), R11"}, + {[]byte{0xf3, 0x4c, 0x0f, 0x38, 0xf6, 0xda}, "ADOXQ DX, R11"}, + {[]byte{0xf3, 0x4d, 0x0f, 0x38, 0xf6, 0xdb}, "ADOXQ R11, R11"}, + // amd64enc.s: RDSEEDW DX // 660fc7fa. + {[]byte{0x66, 0x0f, 0xc7, 0xfa}, "RDSEEDW DX"}, + {[]byte{0x66, 0x41, 0x0f, 0xc7, 0xfb}, "RDSEEDW R11"}, + // amd64enc_extra.s: RDPID DX // f30fc7fa. + {[]byte{0xf3, 0x0f, 0xc7, 0xfa}, "RDPID DX"}, + {[]byte{0xf3, 0x41, 0x0f, 0xc7, 0xfb}, "RDPID R11"}, + // amd64enc_extra.s: TPAUSE BX // 660faef3 and kin. + {[]byte{0x66, 0x0f, 0xae, 0xf3}, "TPAUSE BX"}, + {[]byte{0xf3, 0x0f, 0xae, 0xf3}, "UMONITOR BX"}, + {[]byte{0xf2, 0x0f, 0xae, 0xf3}, "UMWAIT BX"}, + // amd64enc_extra.s: ENDBR64 // f30f1efa; ENDBR32 has no + // toolchain spelling. + {[]byte{0xf3, 0x0f, 0x1e, 0xfa}, "ENDBR64"}, + {[]byte{0xf3, 0x0f, 0x1e, 0xfb}, "ENDBR32"}, + } { + ins, err := Decode(arch.AMD64, 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)) + } + } +} + +// TestDegenerateNamingBoundaries guards the table's edges: bytes the +// decoder rejects outright keep the placeholder, and the prefix +// combinations no toolchain spelling carries stay unnamed even where the +// decode is degenerate. The bare 0F 38 F6 form is the 32-bit ADCX of the +// Intel manual; the toolchain's ADCXL spelling always carries the +// operand-size override, so the bare form is left to the placeholder +// rather than named to a spelling that would re-encode differently. +func TestDegenerateNamingBoundaries(t *testing.T) { + for _, tt := range []struct { + code []byte + text string + }{ + // Rejected outright: RDSEED without the operand-size override + // (the bare 0F C7 /7 register form), MONITORX and MWAITX, the + // hint NOP CLDEMOTE. + {[]byte{0x0f, 0xc7, 0xfa}, "???"}, + {[]byte{0x0f, 0x01, 0xfa}, "???"}, + {[]byte{0x0f, 0x01, 0xfb}, "???"}, + {[]byte{0x0f, 0x1c, 0x03}, "???"}, + // Degenerate but outside the table's prefix gates: repne ADCX is + // no instruction the corpus names. + {[]byte{0xf2, 0x0f, 0x38, 0xf6, 0xd2}, "REPNE; Op(0)"}, + // MONITOR and MWAIT decode as named opcodes and never reach the + // table. + {[]byte{0x0f, 0x01, 0xc8}, "MONITOR"}, + {[]byte{0x0f, 0x01, 0xc9}, "MWAIT"}, + } { + ins, err := Decode(arch.AMD64, 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/testdata/parity_amd64_unlisted.txt b/disasm/testdata/parity_amd64_unlisted.txt index 5c57b54..adf59c6 100644 --- a/disasm/testdata/parity_amd64_unlisted.txt +++ b/disasm/testdata/parity_amd64_unlisted.txt @@ -1188,3 +1188,43 @@ c4e17c77 VZEROALL c4e17877 VZEROUPPER 0f09 WBINVD f30faed2 WRFSBASE DX +660f38f613 ADCXL 0(BX), DX +66410f38f613 ADCXL 0(R11), DX +660f38f6d2 ADCXL DX, DX +66410f38f6d3 ADCXL R11, DX +66440f38f61b ADCXL 0(BX), R11 +66450f38f61b ADCXL 0(R11), R11 +66440f38f6da ADCXL DX, R11 +66450f38f6db ADCXL R11, R11 +66480f38f613 ADCXQ 0(BX), DX +66490f38f613 ADCXQ 0(R11), DX +66480f38f6d2 ADCXQ DX, DX +66490f38f6d3 ADCXQ R11, DX +664c0f38f61b ADCXQ 0(BX), R11 +664d0f38f61b ADCXQ 0(R11), R11 +664c0f38f6da ADCXQ DX, R11 +664d0f38f6db ADCXQ R11, R11 +f30f38f613 ADOXL 0(BX), DX +f3410f38f613 ADOXL 0(R11), DX +f30f38f6d2 ADOXL DX, DX +f3410f38f6d3 ADOXL R11, DX +f3440f38f61b ADOXL 0(BX), R11 +f3450f38f61b ADOXL 0(R11), R11 +f3440f38f6da ADOXL DX, R11 +f3450f38f6db ADOXL R11, R11 +f3480f38f613 ADOXQ 0(BX), DX +f3490f38f613 ADOXQ 0(R11), DX +f3480f38f6d2 ADOXQ DX, DX +f3490f38f6d3 ADOXQ R11, DX +f34c0f38f61b ADOXQ 0(BX), R11 +f34d0f38f61b ADOXQ 0(R11), R11 +f34c0f38f6da ADOXQ DX, R11 +f34d0f38f6db ADOXQ R11, R11 +660fc7fa RDSEEDW DX +66410fc7fb RDSEEDW R11 +f30fc7fa RDPID DX +f3410fc7fb RDPID R11 +660faef3 TPAUSE BX +f30faef3 UMONITOR BX +f20faef3 UMWAIT BX +f30f1efa ENDBR64