feat(disasm): name the amd64 encodings x86asm refuses

The toolchain's assembler corpus carries 195 amd64 encodings the
x/arch decoder rejects or degenerates: the BMI1/BMI2 VEX families
(ANDN, BEXTR, BLSI, BLSMSK, BLSR, BZHI, MULX, PDEP, PEXT, RORX,
SARX, SHLX, SHRX), the 0F 01 quartet CLAC, STAC, RDPKRU and WRPKRU,
the bare and REX-only RDSEED forms, and UD1.  The supplementary
naming table decodes the VEX prefix and the ModR/M shape and renders
the toolchain's own spellings; every corpus row is pinned in the
unlisted fixture and round-trips byte for byte through the encoder,
and the boundary test pins the prefix shapes no family carries.

Assisted-by: GLM 5.3
This commit is contained in:
petrbalvin committed 2026-10-07 13:49:58 +02:00
1 parent d98aadbbbf
commit 2c70359ad0
3 files changed
+419 -16

No files matched your search

+166 -11
View File
@@ -194,7 +194,18 @@ func (rm amd64RM) text() string {
// 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 {
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:]
@@ -320,22 +331,166 @@ func nameAMD64Endbr(p amd64Prefixes, tail []byte) (string, int, bool) {
}
// nameAMD64Rejected names an amd64 encoding the decoder refuses outright,
// one family at a time as the corpus rows land. CLDEMOTE, NP 0F 1C /r
// with a memory operand, is the first: the toolchain's own table carries
// it as a memory-only instruction, and the decoder rejects the encoding
// instead of naming it. The register forms of the same opcode are the
// hint NOPs the corpus does not spell, and they stay rejected.
// 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 len(code) < p.n+3 || code[p.n] != 0x0f || code[p.n+1] != 0x1c {
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
}
rm, ok := decodeAMD64RM(code[p.n+2:], p.rexR, p.rexX, p.rexB)
if !ok || rm.regForm {
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 "CLDEMOTE " + rm.text(), p.n + 2 + rm.n, true
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
}