// 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+2 || code[p.n] != 0x0f { return "", 0, false } // UD1, the second undefined-instruction opcode: the toolchain's table // carries it with no operands, exactly two bytes, so the listing // consumes nothing behind them. Any bytes that follow belong to the // next instruction. if code[p.n+1] == 0xb9 && !p.osz && !p.rep && !p.repne && !p.rexW && !p.rexR && !p.rexX && !p.rexB { return "UD1", p.n + 2, true } if len(code) < p.n+3 { 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 } // nameAMD64Rejected names an amd64 encoding the decoder refuses outright, // one family at a time as the corpus rows land. Three kinds live here: // CLDEMOTE, NP 0F 1C /r with a memory operand, the toolchain's own table // being a memory-only instruction while the decoder rejects the encoding; // the register forms of the same opcode are the hint NOPs the corpus does // not spell, and they stay rejected. The 0F 01 pair CLAC/STAC and the // protection-key pair RDPKRU/WRPKRU, fixed three-byte encodings no ModR/M // shape distinguishes, which the decoder rejects although the corpus // assembles them. And the BMI1/BMI2 VEX families below. func nameAMD64Rejected(code []byte) (string, int, bool) { p, ok := scanAMD64Prefixes(code) if !ok || p.osz || p.rep || p.repne { return "", 0, false } if p.n == 0 && len(code) > 0 && code[0] == 0xc4 { return nameAMD64VEX(code) } if len(code) < p.n+3 || code[p.n] != 0x0f { return "", 0, false } switch code[p.n+1] { case 0x1c: rm, ok := decodeAMD64RM(code[p.n+2:], p.rexR, p.rexX, p.rexB) if !ok || rm.regForm { return "", 0, false } return "CLDEMOTE " + rm.text(), p.n + 2 + rm.n, true case 0x01: if !p.rexW && !p.rexR && !p.rexX && !p.rexB { switch code[p.n+2] { case 0xca: return "CLAC", p.n + 3, true case 0xcb: return "STAC", p.n + 3, true case 0xee: return "RDPKRU", p.n + 3, true case 0xef: return "WRPKRU", p.n + 3, true } } case 0xc7: // The error branch of the RDSEED family: the operand-size and // rep forms come back degenerate (handled above), while the // bare and REX-only forms are refused outright. if !p.rexR && !p.rexX { return nameAMD64RNG(p, code[p.n+2:]) } } return "", 0, false } // amd64VEX is the reading of one three-byte VEX prefix, C4 rx bm wlpp. // REX extension, the escaped opcode map and the payload byte are decoded // once here; the families below are keyed on the pieces. type amd64VEX struct { r, x, b bool // register-extension bits, REX-style w bool // operand-width bit vvvv int // the inverted operand-register field l bool // vector-length bit, clear in every GPR family pp int // the legacy-prefix selector m int // the escaped opcode map, 2 for 0F38 and 3 for 0F3A n int // bytes consumed: C4 plus its two payload bytes } // parseAMD64VEX reads the C4 prefix at the start of code. A two-byte C5 // VEX is not read: every corpus family here is three-byte. func parseAMD64VEX(code []byte) (amd64VEX, bool) { var v amd64VEX if len(code) < 4 || code[0] != 0xc4 { return v, false } b1, b2 := code[1], code[2] v.r = b1&0x80 == 0 v.x = b1&0x40 == 0 v.b = b1&0x20 == 0 v.m = int(b1 & 0x1f) v.w = b2&0x80 != 0 v.vvvv = int(^b2>>3) & 15 v.l = b2&0x04 != 0 v.pp = int(b2 & 0x03) v.n = 3 return v, true } // nameAMD64VEX names the BMI1/BMI2 general-purpose VEX families the decoder // refuses with "unknown AVX Opcode". Every family is pinned to the prefix // selector, opcode map and operand arrangement the toolchain's corpus // carries; a byte pattern outside those (the vector-length bit set, a // different selector, a ModR/M reg field the family does not define) keeps // the placeholder. func nameAMD64VEX(code []byte) (string, int, bool) { v, ok := parseAMD64VEX(code) if !ok || v.l || v.m < 2 || v.m > 3 { return "", 0, false } opcode := code[v.n] rm, ok := decodeAMD64RM(code[v.n+1:], v.r, v.x, v.b) if !ok { return "", 0, false } suffix := "L" if v.w { suffix = "Q" } reg := amd64GPRNames[rm.reg] vvvv := amd64GPRNames[v.vvvv] // Families on the 0F38 map. if v.m == 2 { switch { case opcode == 0xf2 && v.pp == 0x0: // ANDN rm, vvvv, reg. return "ANDN" + suffix + " " + rm.text() + ", " + vvvv + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf3 && v.pp == 0x0: // The three one-source pseudo-instructions share the // opcode: the ModR/M reg field selects the family. var name string switch rm.reg { case 1: name = "BLSR" case 2: name = "BLSMSK" case 3: name = "BLSI" } if name == "" { return "", 0, false } return name + suffix + " " + rm.text() + ", " + vvvv, v.n + 1 + rm.n, true case opcode == 0xf5 && v.pp == 0x0: // BZHI vvvv, rm, reg. return "BZHI" + suffix + " " + vvvv + ", " + rm.text() + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf6 && v.pp == 0x3: // MULX rm, vvvv, reg. return "MULX" + suffix + " " + rm.text() + ", " + vvvv + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf5 && v.pp == 0x3: // PDEP rm, vvvv, reg. return "PDEP" + suffix + " " + rm.text() + ", " + vvvv + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf5 && v.pp == 0x2: // PEXT rm, vvvv, reg. return "PEXT" + suffix + " " + rm.text() + ", " + vvvv + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf7 && v.pp == 0x0: // BEXTR vvvv, rm, reg. return "BEXTR" + suffix + " " + vvvv + ", " + rm.text() + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf7 && v.pp == 0x2: // SARX vvvv, rm, reg. return "SARX" + suffix + " " + vvvv + ", " + rm.text() + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf7 && v.pp == 0x1: // SHLX vvvv, rm, reg. return "SHLX" + suffix + " " + vvvv + ", " + rm.text() + ", " + reg, v.n + 1 + rm.n, true case opcode == 0xf7 && v.pp == 0x3: // SHRX vvvv, rm, reg. return "SHRX" + suffix + " " + vvvv + ", " + rm.text() + ", " + reg, v.n + 1 + rm.n, true } return "", 0, false } // The 0F3A map: RORX $imm, rm, reg, with a dead vvvv field. if opcode == 0xf0 && v.pp == 0x3 && v.vvvv == 0 && v.n+1+rm.n < len(code) { imm := int8(code[v.n+1+rm.n]) return fmt.Sprintf("RORX%s $%d, %s, %s", suffix, imm, rm.text(), reg), v.n + 2 + rm.n, true } return "", 0, false }