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:
1 parent
d98aadbbbf
commit
2c70359ad0
3 files changed
+419
-16
No files matched your search
+166
-11
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user