fix(disasm): drop the implicit GOARCH constraint the naming files carried
The names naming_amd64.go and naming_amd64_test.go carried the _amd64 filename suffix, which the go tool reads as an implicit GOARCH=amd64 build constraint: the tables disappeared from every non-amd64 build and the package failed to compile for arm64, riscv64 and loong64, the other three architectures the tool assembles. Renaming to amd64_naming.go removes the constraint; the module builds again for all four GOARCH values. Assisted-by: GLM 5.3
This commit is contained in:
1 parent
2c70359ad0
commit
3f35b2a718
2 files changed
No files matched your search
@@ -0,0 +1,496 @@
|
||||
// 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+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
|
||||
}
|
||||
Reference in new issue
Block a user