Files
gasm-sdk/disasm/naming_amd64.go
T
petrbalvin 458d981f31 feat(disasm): name the CLDEMOTE encoding the decoder refuses
The hint NOP opcode 0F 1C /r with a memory operand is CLDEMOTE, a
memory-only instruction the toolchain's own table carries; the decoder
rejects the encoding instead of naming it.  The rejected-encoding side
of the supplementary table names it from the bytes, and the corpus row
0f1c03 pins the text in the unlisted fixture, round trip byte exact.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:27:51 +02:00

342 lines
9.7 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
}
// 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.
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 {
return "", 0, false
}
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
}