kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

isa.c (44678B)


      1 /* x86_64 instruction descriptor table + operand print/decode dispatch.
      2  *
      3  * The table mirrors every encoding `src/arch/x64/emit.c` produces, plus a
      4  * handful that show up via direct byte writes in arch/x64/{alloc,link,ops}.c
      5  * (CALL/JMP rel32, PUSH/POP r64, multi-byte NOP, atomic prefixes).  Each
      6  * row pins down (leg_pfx, opcode bytes, /digit) so the disassembler can
      7  * identify a raw byte stream with one linear pass and then dispatch on
      8  * the format to render operands.
      9  *
     10  * Row ordering: first-match wins.  Aliases (rows with X64_ASMFL_ALIAS)
     11  * sit BEFORE the canonical row they alias so the disassembler prefers
     12  * the alias spelling on output.  We keep aliases narrow today (e.g.,
     13  * SSE-prefixed forms naturally precede their no-prefix neighbours) — we
     14  * can add `xor %eax,%eax` zeroing-idiom aliases later if disasm output
     15  * needs them. */
     16 
     17 #include "arch/x64/isa.h"
     18 
     19 #include <stddef.h>
     20 #include <string.h>
     21 
     22 #include "core/bytes.h"
     23 
     24 /* ====================================================================
     25  * Table. Mnemonics are AT&T-style, lower-case, no size suffix; the
     26  * printer derives the size letter (b/w/l/q) from the fmt + REX.W where
     27  * appropriate.
     28  * ==================================================================== */
     29 
     30 #define ROW(mn, lp, ol, b0, b1, b2, lm, mr, wr, f, fl) \
     31   {{{(mn)}, sizeof(mn) - 1}, lp, ol, {b0, b1, b2}, lm, mr, wr, f, fl}
     32 #define NO_MODRM 0xFFu
     33 
     34 const X64InsnDesc x64_insn_table[] = {
     35     /* ---- single-byte nullary ---- */
     36     ROW("nop", X64_PFX_NONE, 1, 0x90, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     37         X64_FMT_NULLARY, 0),
     38     ROW("ret", X64_PFX_NONE, 1, 0xC3, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     39         X64_FMT_NULLARY, 0),
     40     ROW("leave", X64_PFX_NONE, 1, 0xC9, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     41         X64_FMT_NULLARY, 0),
     42     ROW("cltd", X64_PFX_NONE, 1, 0x99, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
     43         X64_FMT_NULLARY, 0),
     44     ROW("cqto", X64_PFX_NONE, 1, 0x99, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
     45         X64_FMT_NULLARY, 0),
     46 
     47     /* ---- two-byte UD2 ---- */
     48     ROW("ud2", X64_PFX_NONE, 2, 0x0F, 0x0B, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     49         X64_FMT_NULLARY, 0),
     50     /* ---- SYSCALL (0F 05): fast system call ---- */
     51     ROW("syscall", X64_PFX_NONE, 2, 0x0F, 0x05, 0, 0xFF, NO_MODRM,
     52         X64_W_REQ_ANY, X64_FMT_NULLARY, 0),
     53     /* ---- RDTSC (0F 31): read timestamp counter into edx:eax ---- */
     54     ROW("rdtsc", X64_PFX_NONE, 2, 0x0F, 0x31, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     55         X64_FMT_NULLARY, 0),
     56     ROW("mfence", X64_PFX_NONE, 3, 0x0F, 0xAE, 0xF0, 0xFF, NO_MODRM,
     57         X64_W_REQ_ANY, X64_FMT_NULLARY, 0),
     58 
     59     /* ---- multi-byte NOP: 66 0F 1F /0 ----
     60      * Matches the 6-byte canonical "NOPW 0(%rax,%rax,1)" kit emits to pad
     61      * the IPLT stub. The mod/rm bytes (and any disp) are consumed by the
     62      * NOP_MULTI printer. */
     63     ROW("nopw", X64_PFX_66, 2, 0x0F, 0x1F, 0, 0xFF, 0, X64_W_REQ_ANY,
     64         X64_FMT_NOP_MULTI, 0),
     65     ROW("nopl", X64_PFX_NONE, 2, 0x0F, 0x1F, 0, 0xFF, 0, X64_W_REQ_ANY,
     66         X64_FMT_NOP_MULTI, 0),
     67 
     68     /* ---- PUSH/POP r64 (embed-reg in low 3 bits) ---- */
     69     ROW("push", X64_PFX_NONE, 1, 0x50, 0, 0, 0xF8, NO_MODRM, X64_W_REQ_ANY,
     70         X64_FMT_PUSH_POP, X64_ASMFL_FORCE_W64),
     71     ROW("pop", X64_PFX_NONE, 1, 0x58, 0, 0, 0xF8, NO_MODRM, X64_W_REQ_ANY,
     72         X64_FMT_PUSH_POP, X64_ASMFL_FORCE_W64),
     73 
     74     /* ---- MOV r, imm — B8+rd; width via REX.W ----
     75      * imm32 form (no REX.W) and imm64 movabs form (REX.W=1) share the
     76      * same row; the printer reads ctx->rex_w to pick the imm width. */
     77     ROW("mov", X64_PFX_NONE, 1, 0xB8, 0, 0, 0xF8, NO_MODRM, X64_W_REQ_ANY,
     78         X64_FMT_MOV_RI, X64_ASMFL_W_FROM_REX),
     79 
     80     /* ---- ALU r/m, r — opcode picks op ---- */
     81     ROW("add", X64_PFX_NONE, 1, 0x01, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     82         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     83     ROW("or", X64_PFX_NONE, 1, 0x09, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     84         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     85     ROW("and", X64_PFX_NONE, 1, 0x21, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     86         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     87     ROW("sub", X64_PFX_NONE, 1, 0x29, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     88         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     89     ROW("xor", X64_PFX_NONE, 1, 0x31, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     90         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     91     ROW("cmp", X64_PFX_NONE, 1, 0x39, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     92         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     93     ROW("test", X64_PFX_NONE, 1, 0x85, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     94         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     95     ROW("mov", X64_PFX_NONE, 1, 0x89, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     96         X64_FMT_ALU_RR, X64_ASMFL_W_FROM_REX),
     97     /* Byte form: MOV r/m8, r8 — opcode 88 forces 1-byte operands. */
     98     ROW("mov", X64_PFX_NONE, 1, 0x88, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
     99         X64_FMT_ALU_RR, X64_ASMFL_BYTE),
    100     /* 16-bit form: 0x66 prefix forces 2-byte operands. */
    101     ROW("mov", X64_PFX_66, 1, 0x89, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    102         X64_FMT_ALU_RR, X64_ASMFL_W16),
    103 
    104     /* ---- MOV r, r/m  (load and reg-reg share opcode 8B) ----
    105      * 8B /r matches both r,r and r,[base+disp]; the printer dispatches on
    106      * ModR/M.mod. LEA is 8D /r — register-only ModR/M.mod=11 is illegal,
    107      * so we use a separate row keyed on the opcode. */
    108     ROW("mov", X64_PFX_NONE, 1, 0x8B, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    109         X64_FMT_MOV_RM_LOAD, X64_ASMFL_W_FROM_REX),
    110     /* 16-bit r←r/m via 0x66 prefix. */
    111     ROW("mov", X64_PFX_66, 1, 0x8B, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    112         X64_FMT_MOV_RM_LOAD, X64_ASMFL_W16),
    113     ROW("lea", X64_PFX_NONE, 1, 0x8D, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    114         X64_FMT_MOV_RM_LOAD, X64_ASMFL_W_FROM_REX),
    115 
    116     /* ---- MOVZX / MOVSX r{32,64}, r/m{8,16} ----
    117      * The destination width is the *l* (32-bit) form without REX.W and the *q*
    118      * (64-bit) form with it; split by W so the disassembler emits a mnemonic
    119      * whose size letter matches the printed register width (clang rejects a
    120      * `movsbl` with a 64-bit destination). Same opcodes; W disambiguates,
    121      * exactly like cltd/cqto (0x99). */
    122     ROW("movzbl", X64_PFX_NONE, 2, 0x0F, 0xB6, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
    123         X64_FMT_MOVZX_MOVSX, 0),
    124     ROW("movzbq", X64_PFX_NONE, 2, 0x0F, 0xB6, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
    125         X64_FMT_MOVZX_MOVSX, 0),
    126     ROW("movzwl", X64_PFX_NONE, 2, 0x0F, 0xB7, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
    127         X64_FMT_MOVZX_MOVSX, 0),
    128     ROW("movzwq", X64_PFX_NONE, 2, 0x0F, 0xB7, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
    129         X64_FMT_MOVZX_MOVSX, 0),
    130     ROW("movsbl", X64_PFX_NONE, 2, 0x0F, 0xBE, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
    131         X64_FMT_MOVZX_MOVSX, 0),
    132     ROW("movsbq", X64_PFX_NONE, 2, 0x0F, 0xBE, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
    133         X64_FMT_MOVZX_MOVSX, 0),
    134     ROW("movswl", X64_PFX_NONE, 2, 0x0F, 0xBF, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
    135         X64_FMT_MOVZX_MOVSX, 0),
    136     ROW("movswq", X64_PFX_NONE, 2, 0x0F, 0xBF, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
    137         X64_FMT_MOVZX_MOVSX, 0),
    138 
    139     /* ---- MOVSXD r64, r/m32 ---- */
    140     ROW("movslq", X64_PFX_NONE, 1, 0x63, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
    141         X64_FMT_MOVSXD, 0),
    142 
    143     /* ---- ALU r/m, imm — /digit picks operation ----
    144      * 83 (imm8 sign-extended), 81 (imm32 sign-extended). One row per
    145      * (opcode, /digit) pair.
    146      *   /0 ADD  /1 OR   /4 AND  /5 SUB  /6 XOR  /7 CMP
    147      * (/2 ADC and /3 SBB are also valid in the Intel manual but kit
    148      *  doesn't emit them; they can land later as additional rows.) */
    149     ROW("add", X64_PFX_NONE, 1, 0x83, 0, 0, 0xFF, 0, X64_W_REQ_ANY,
    150         X64_FMT_ALU_RM_IMM8, X64_ASMFL_W_FROM_REX),
    151     ROW("or", X64_PFX_NONE, 1, 0x83, 0, 0, 0xFF, 1, X64_W_REQ_ANY,
    152         X64_FMT_ALU_RM_IMM8, X64_ASMFL_W_FROM_REX),
    153     ROW("and", X64_PFX_NONE, 1, 0x83, 0, 0, 0xFF, 4, X64_W_REQ_ANY,
    154         X64_FMT_ALU_RM_IMM8, X64_ASMFL_W_FROM_REX),
    155     ROW("sub", X64_PFX_NONE, 1, 0x83, 0, 0, 0xFF, 5, X64_W_REQ_ANY,
    156         X64_FMT_ALU_RM_IMM8, X64_ASMFL_W_FROM_REX),
    157     ROW("xor", X64_PFX_NONE, 1, 0x83, 0, 0, 0xFF, 6, X64_W_REQ_ANY,
    158         X64_FMT_ALU_RM_IMM8, X64_ASMFL_W_FROM_REX),
    159     ROW("cmp", X64_PFX_NONE, 1, 0x83, 0, 0, 0xFF, 7, X64_W_REQ_ANY,
    160         X64_FMT_ALU_RM_IMM8, X64_ASMFL_W_FROM_REX),
    161     ROW("add", X64_PFX_NONE, 1, 0x81, 0, 0, 0xFF, 0, X64_W_REQ_ANY,
    162         X64_FMT_ALU_RM_IMM32, X64_ASMFL_W_FROM_REX),
    163     ROW("or", X64_PFX_NONE, 1, 0x81, 0, 0, 0xFF, 1, X64_W_REQ_ANY,
    164         X64_FMT_ALU_RM_IMM32, X64_ASMFL_W_FROM_REX),
    165     ROW("and", X64_PFX_NONE, 1, 0x81, 0, 0, 0xFF, 4, X64_W_REQ_ANY,
    166         X64_FMT_ALU_RM_IMM32, X64_ASMFL_W_FROM_REX),
    167     ROW("sub", X64_PFX_NONE, 1, 0x81, 0, 0, 0xFF, 5, X64_W_REQ_ANY,
    168         X64_FMT_ALU_RM_IMM32, X64_ASMFL_W_FROM_REX),
    169     ROW("xor", X64_PFX_NONE, 1, 0x81, 0, 0, 0xFF, 6, X64_W_REQ_ANY,
    170         X64_FMT_ALU_RM_IMM32, X64_ASMFL_W_FROM_REX),
    171     ROW("cmp", X64_PFX_NONE, 1, 0x81, 0, 0, 0xFF, 7, X64_W_REQ_ANY,
    172         X64_FMT_ALU_RM_IMM32, X64_ASMFL_W_FROM_REX),
    173 
    174     /* ---- IMUL r, r/m (0F AF) / IMUL r, r/m, imm (69 / 6B) ---- */
    175     ROW("imul", X64_PFX_NONE, 2, 0x0F, 0xAF, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    176         X64_FMT_IMUL_RR, X64_ASMFL_W_FROM_REX),
    177     ROW("imul", X64_PFX_NONE, 1, 0x6B, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    178         X64_FMT_IMUL_RRI, X64_ASMFL_W_FROM_REX),
    179     ROW("imul", X64_PFX_NONE, 1, 0x69, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    180         X64_FMT_IMUL_RRI, X64_ASMFL_W_FROM_REX | 0x80u /* imm32 */),
    181 
    182     /* ---- F7 /sub family (no immediate read except for /0 /1 which we
    183      *      don't emit) ---- */
    184     ROW("not", X64_PFX_NONE, 1, 0xF7, 0, 0, 0xFF, 2, X64_W_REQ_ANY,
    185         X64_FMT_F7_RM, X64_ASMFL_W_FROM_REX),
    186     ROW("neg", X64_PFX_NONE, 1, 0xF7, 0, 0, 0xFF, 3, X64_W_REQ_ANY,
    187         X64_FMT_F7_RM, X64_ASMFL_W_FROM_REX),
    188     ROW("mul", X64_PFX_NONE, 1, 0xF7, 0, 0, 0xFF, 4, X64_W_REQ_ANY,
    189         X64_FMT_F7_RM, X64_ASMFL_W_FROM_REX),
    190     ROW("imul", X64_PFX_NONE, 1, 0xF7, 0, 0, 0xFF, 5, X64_W_REQ_ANY,
    191         X64_FMT_F7_RM, X64_ASMFL_W_FROM_REX),
    192     ROW("div", X64_PFX_NONE, 1, 0xF7, 0, 0, 0xFF, 6, X64_W_REQ_ANY,
    193         X64_FMT_F7_RM, X64_ASMFL_W_FROM_REX),
    194     ROW("idiv", X64_PFX_NONE, 1, 0xF7, 0, 0, 0xFF, 7, X64_W_REQ_ANY,
    195         X64_FMT_F7_RM, X64_ASMFL_W_FROM_REX),
    196 
    197     /* ---- Shifts ---- */
    198     ROW("rol", X64_PFX_NONE, 1, 0xC1, 0, 0, 0xFF, 0, X64_W_REQ_ANY,
    199         X64_FMT_SHIFT_IMM, X64_ASMFL_W_FROM_REX),
    200     ROW("ror", X64_PFX_NONE, 1, 0xC1, 0, 0, 0xFF, 1, X64_W_REQ_ANY,
    201         X64_FMT_SHIFT_IMM, X64_ASMFL_W_FROM_REX),
    202     ROW("shl", X64_PFX_NONE, 1, 0xC1, 0, 0, 0xFF, 4, X64_W_REQ_ANY,
    203         X64_FMT_SHIFT_IMM, X64_ASMFL_W_FROM_REX),
    204     ROW("shr", X64_PFX_NONE, 1, 0xC1, 0, 0, 0xFF, 5, X64_W_REQ_ANY,
    205         X64_FMT_SHIFT_IMM, X64_ASMFL_W_FROM_REX),
    206     ROW("sar", X64_PFX_NONE, 1, 0xC1, 0, 0, 0xFF, 7, X64_W_REQ_ANY,
    207         X64_FMT_SHIFT_IMM, X64_ASMFL_W_FROM_REX),
    208     /* 16-bit ROL imm8 via 0x66 + C1 /0 — used by emit_rol16_imm8. */
    209     ROW("rol", X64_PFX_66, 1, 0xC1, 0, 0, 0xFF, 0, X64_W_REQ_ANY,
    210         X64_FMT_SHIFT_IMM, X64_ASMFL_W16),
    211     ROW("shl", X64_PFX_NONE, 1, 0xD3, 0, 0, 0xFF, 4, X64_W_REQ_ANY,
    212         X64_FMT_SHIFT_CL, X64_ASMFL_W_FROM_REX),
    213     ROW("shr", X64_PFX_NONE, 1, 0xD3, 0, 0, 0xFF, 5, X64_W_REQ_ANY,
    214         X64_FMT_SHIFT_CL, X64_ASMFL_W_FROM_REX),
    215     ROW("sar", X64_PFX_NONE, 1, 0xD3, 0, 0, 0xFF, 7, X64_W_REQ_ANY,
    216         X64_FMT_SHIFT_CL, X64_ASMFL_W_FROM_REX),
    217 
    218     /* ---- Branches ---- */
    219     /* Jcc near: 0F 80..8F rel32; condition in low 4 bits. The printer
    220      * picks the mnemonic from a per-condition table. */
    221     ROW("j", X64_PFX_NONE, 2, 0x0F, 0x80, 0, 0xF0, NO_MODRM, X64_W_REQ_ANY,
    222         X64_FMT_JCC_REL32, 0),
    223     ROW("jmp", X64_PFX_NONE, 1, 0xE9, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    224         X64_FMT_JMP_REL32, 0),
    225     ROW("callq", X64_PFX_NONE, 1, 0xE8, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    226         X64_FMT_CALL_REL32, 0),
    227     /* Indirect jmp / call via FF /4 or /2. */
    228     ROW("callq", X64_PFX_NONE, 1, 0xFF, 0, 0, 0xFF, 2, X64_W_REQ_ANY,
    229         X64_FMT_BR_RM, 0),
    230     ROW("jmpq", X64_PFX_NONE, 1, 0xFF, 0, 0, 0xFF, 4, X64_W_REQ_ANY,
    231         X64_FMT_BR_RM, 0),
    232 
    233     /* ---- SETcc / CMOVcc ----
    234      * SETcc condition in low 4 bits of 2nd opcode byte (0F 90..9F).
    235      * CMOVcc same encoding around 0F 40..4F. */
    236     ROW("set", X64_PFX_NONE, 2, 0x0F, 0x90, 0, 0xF0, 0, X64_W_REQ_ANY,
    237         X64_FMT_SETCC_RM, 0),
    238     ROW("cmov", X64_PFX_NONE, 2, 0x0F, 0x40, 0, 0xF0, NO_MODRM, X64_W_REQ_ANY,
    239         X64_FMT_CMOVCC_RR, X64_ASMFL_W_FROM_REX),
    240 
    241     /* ---- BSWAP r — 0F C8+rd ---- */
    242     ROW("bswap", X64_PFX_NONE, 2, 0x0F, 0xC8, 0, 0xF8, NO_MODRM, X64_W_REQ_ANY,
    243         X64_FMT_BSWAP, X64_ASMFL_W_FROM_REX),
    244 
    245     /* ---- Bit scan: BSF / BSR ---- */
    246     ROW("bsf", X64_PFX_NONE, 2, 0x0F, 0xBC, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    247         X64_FMT_BS, X64_ASMFL_W_FROM_REX),
    248     ROW("bsr", X64_PFX_NONE, 2, 0x0F, 0xBD, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    249         X64_FMT_BS, X64_ASMFL_W_FROM_REX),
    250 
    251     /* ---- POPCNT — F3 0F B8 /r (note: F3 prefix is REQUIRED) ---- */
    252     ROW("popcnt", X64_PFX_F3, 2, 0x0F, 0xB8, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    253         X64_FMT_POPCNT, X64_ASMFL_W_FROM_REX),
    254 
    255     /* ---- Atomic primitives ---- */
    256     /* XADD m, r — 0F C1 /r (LOCK prefix is decoded separately) */
    257     ROW("xadd", X64_PFX_NONE, 2, 0x0F, 0xC1, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    258         X64_FMT_XADD_MEM, X64_ASMFL_W_FROM_REX),
    259     /* XCHG r, r/m — 0x87 /r */
    260     ROW("xchg", X64_PFX_NONE, 1, 0x87, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    261         X64_FMT_XCHG_MEM, X64_ASMFL_W_FROM_REX),
    262     /* CMPXCHG m, r — 0F B1 /r */
    263     ROW("cmpxchg", X64_PFX_NONE, 2, 0x0F, 0xB1, 0, 0xFF, NO_MODRM,
    264         X64_W_REQ_ANY, X64_FMT_CMPXCHG_MEM, X64_ASMFL_W_FROM_REX),
    265 
    266     /* ---- SSE scalar FP — F2/F3 0F xx /r ----
    267      * Three opcodes per (sd, ss) pair: arith / mov / cmp.  Each row pairs
    268      * the legacy prefix (selects sd vs ss) with the 0F xx /r opcode. */
    269     /* MOVSS / MOVSD */
    270     ROW("movsd", X64_PFX_F2, 2, 0x0F, 0x10, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    271         X64_FMT_SSE_RR, 0),
    272     ROW("movsd", X64_PFX_F2, 2, 0x0F, 0x11, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    273         X64_FMT_SSE_RR, X64_ASMFL_ALIAS),
    274     ROW("movss", X64_PFX_F3, 2, 0x0F, 0x10, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    275         X64_FMT_SSE_RR, 0),
    276     ROW("movss", X64_PFX_F3, 2, 0x0F, 0x11, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    277         X64_FMT_SSE_RR, X64_ASMFL_ALIAS),
    278     /* MOVAPS */
    279     ROW("movaps", X64_PFX_NONE, 2, 0x0F, 0x28, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    280         X64_FMT_SSE_RR, 0),
    281     ROW("movaps", X64_PFX_NONE, 2, 0x0F, 0x29, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    282         X64_FMT_SSE_RR, X64_ASMFL_ALIAS),
    283     /* ADD/SUB/MUL/DIV — opcodes 58/5C/59/5E (same byte for ss and sd;
    284      * prefix picks). */
    285     ROW("addsd", X64_PFX_F2, 2, 0x0F, 0x58, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    286         X64_FMT_SSE_RR, 0),
    287     ROW("addss", X64_PFX_F3, 2, 0x0F, 0x58, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    288         X64_FMT_SSE_RR, 0),
    289     ROW("mulsd", X64_PFX_F2, 2, 0x0F, 0x59, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    290         X64_FMT_SSE_RR, 0),
    291     ROW("mulss", X64_PFX_F3, 2, 0x0F, 0x59, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    292         X64_FMT_SSE_RR, 0),
    293     ROW("subsd", X64_PFX_F2, 2, 0x0F, 0x5C, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    294         X64_FMT_SSE_RR, 0),
    295     ROW("subss", X64_PFX_F3, 2, 0x0F, 0x5C, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    296         X64_FMT_SSE_RR, 0),
    297     ROW("divsd", X64_PFX_F2, 2, 0x0F, 0x5E, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    298         X64_FMT_SSE_RR, 0),
    299     ROW("divss", X64_PFX_F3, 2, 0x0F, 0x5E, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    300         X64_FMT_SSE_RR, 0),
    301     /* Compare scalar (UCOMISS / UCOMISD) */
    302     ROW("ucomisd", X64_PFX_66, 2, 0x0F, 0x2E, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    303         X64_FMT_SSE_RR, 0),
    304     ROW("ucomiss", X64_PFX_NONE, 2, 0x0F, 0x2E, 0, 0xFF, NO_MODRM,
    305         X64_W_REQ_ANY, X64_FMT_SSE_RR, 0),
    306     /* Conversions touched by FP↔int paths: CVTSI2SS/SD, CVTTSS/SD2SI. */
    307     ROW("cvtsi2sd", X64_PFX_F2, 2, 0x0F, 0x2A, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    308         X64_FMT_SSE_RR, X64_ASMFL_W_FROM_REX),
    309     ROW("cvtsi2ss", X64_PFX_F3, 2, 0x0F, 0x2A, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    310         X64_FMT_SSE_RR, X64_ASMFL_W_FROM_REX),
    311     ROW("cvttsd2si", X64_PFX_F2, 2, 0x0F, 0x2C, 0, 0xFF, NO_MODRM,
    312         X64_W_REQ_ANY, X64_FMT_SSE_RR, X64_ASMFL_W_FROM_REX),
    313     ROW("cvttss2si", X64_PFX_F3, 2, 0x0F, 0x2C, 0, 0xFF, NO_MODRM,
    314         X64_W_REQ_ANY, X64_FMT_SSE_RR, X64_ASMFL_W_FROM_REX),
    315     ROW("cvtsd2ss", X64_PFX_F2, 2, 0x0F, 0x5A, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    316         X64_FMT_SSE_RR, 0),
    317     ROW("cvtss2sd", X64_PFX_F3, 2, 0x0F, 0x5A, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    318         X64_FMT_SSE_RR, 0),
    319     /* MOVD/MOVQ between GPR and XMM. 66 0F 6E /r is gpr->xmm, 66 0F 7E /r is
    320      * xmm->gpr (note the reversed operand order, handled in print_xmm_rr).
    321      * REX.W picks movq (64-bit GPR) vs movd (32-bit), and since the *mnemonic*
    322      * itself changes we split into W_REQ_0 / W_REQ_1 rows rather than a width
    323      * suffix. The backend emits these for int<->FP bitcasts (emit_sse_rr_w). */
    324     ROW("movd", X64_PFX_66, 2, 0x0F, 0x6E, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
    325         X64_FMT_SSE_RR, 0),
    326     ROW("movq", X64_PFX_66, 2, 0x0F, 0x6E, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
    327         X64_FMT_SSE_RR, 0),
    328     ROW("movd", X64_PFX_66, 2, 0x0F, 0x7E, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
    329         X64_FMT_SSE_RR, 0),
    330     ROW("movq", X64_PFX_66, 2, 0x0F, 0x7E, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
    331         X64_FMT_SSE_RR, 0),
    332     /* XORPS / XORPD (0F 57, prefix selects packed-single vs -double). The
    333      * backend uses these to clear/negate FP registers. Both operands xmm. */
    334     ROW("xorps", X64_PFX_NONE, 2, 0x0F, 0x57, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    335         X64_FMT_SSE_RR, 0),
    336     ROW("xorpd", X64_PFX_66, 2, 0x0F, 0x57, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
    337         X64_FMT_SSE_RR, 0),
    338 };
    339 
    340 const u32 x64_insn_table_n =
    341     (u32)(sizeof x64_insn_table / sizeof x64_insn_table[0]);
    342 
    343 /* ====================================================================
    344  * Prefix decode.
    345  * ==================================================================== */
    346 
    347 u32 x64_decode_prefixes(const u8* bytes, u32 len, X64DecodeCtx* ctx) {
    348   u32 off = 0;
    349   memset(ctx, 0, sizeof *ctx);
    350   while (off < len) {
    351     u8 b = bytes[off];
    352     if (b == 0x66u || b == 0xF2u || b == 0xF3u) {
    353       ctx->leg_pfx = b;
    354       ++off;
    355       continue;
    356     }
    357     if (b == 0xF0u) {
    358       /* LOCK — ignored for opcode lookup but consumed so the
    359        * subsequent opcode aligns. The printer adds a "lock " prefix
    360        * separately when annotating, but kit's emit.c currently emits
    361        * LOCK only before XADD / XCHG / CMPXCHG. */
    362       ctx->has_lock = 1;
    363       ++off;
    364       continue;
    365     }
    366     break;
    367   }
    368   if (off < len && bytes[off] >= 0x40u && bytes[off] <= 0x4Fu) {
    369     u8 r = bytes[off];
    370     ctx->has_rex = 1;
    371     ctx->rex_w = (r >> 3) & 1u;
    372     ctx->rex_r = (r >> 2) & 1u;
    373     ctx->rex_x = (r >> 1) & 1u;
    374     ctx->rex_b = r & 1u;
    375     ++off;
    376   }
    377   ctx->opc_off = off;
    378   return off;
    379 }
    380 
    381 /* ====================================================================
    382  * Disassembler row lookup.
    383  * ==================================================================== */
    384 
    385 const X64InsnDesc* x64_disasm_find(const u8* bytes, u32 len,
    386                                    X64DecodeCtx* ctx) {
    387   if (ctx->opc_off >= len) return NULL;
    388   for (u32 i = 0; i < x64_insn_table_n; ++i) {
    389     const X64InsnDesc* d = &x64_insn_table[i];
    390     if (d->leg_pfx != ctx->leg_pfx) continue;
    391     if (d->rex_w_req == X64_W_REQ_1 && !ctx->rex_w) continue;
    392     if (d->rex_w_req == X64_W_REQ_0 && ctx->rex_w) continue;
    393     if (ctx->opc_off + d->opc_len > len) continue;
    394     /* Opcode bytes match exactly except the LAST byte, which may use
    395      * a low-bit mask (embed-reg or condition nibble). */
    396     int ok = 1;
    397     for (u32 j = 0; j + 1u < d->opc_len; ++j) {
    398       if (bytes[ctx->opc_off + j] != d->opc[j]) {
    399         ok = 0;
    400         break;
    401       }
    402     }
    403     if (!ok) continue;
    404     {
    405       u8 last_act = bytes[ctx->opc_off + d->opc_len - 1u] & d->opc_last_mask;
    406       u8 last_exp = d->opc[d->opc_len - 1u] & d->opc_last_mask;
    407       if (last_act != last_exp) continue;
    408     }
    409     /* /digit constraint reads ModR/M.reg. */
    410     if (d->modrm_reg != NO_MODRM) {
    411       u32 mrm_off = ctx->opc_off + d->opc_len;
    412       if (mrm_off >= len) continue;
    413       u8 mrm = bytes[mrm_off];
    414       if (((mrm >> 3) & 7u) != d->modrm_reg) continue;
    415     }
    416     return d;
    417   }
    418   return NULL;
    419 }
    420 
    421 /* ====================================================================
    422  * Operand printers.
    423  * ==================================================================== */
    424 
    425 #define X64_REG_RIP 16u
    426 
    427 static const char* g_cc_name[16] = {
    428     "o", "no", "b", "ae", "e", "ne", "be", "a",
    429     "s", "ns", "p", "np", "l", "ge", "le", "g",
    430 };
    431 
    432 /* AT&T register names by width. Index 0..15 covers RAX..R15. */
    433 static const char* reg_name(u32 reg, u32 width_bytes, int has_rex) {
    434   static const char* r64[16] = {
    435       "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi",
    436       "r8",  "r9",  "r10", "r11", "r12", "r13", "r14", "r15",
    437   };
    438   static const char* r32[16] = {
    439       "eax", "ecx", "edx",  "ebx",  "esp",  "ebp",  "esi",  "edi",
    440       "r8d", "r9d", "r10d", "r11d", "r12d", "r13d", "r14d", "r15d",
    441   };
    442   static const char* r16[16] = {
    443       "ax",  "cx",  "dx",   "bx",   "sp",   "bp",   "si",   "di",
    444       "r8w", "r9w", "r10w", "r11w", "r12w", "r13w", "r14w", "r15w",
    445   };
    446   static const char* r8[16] = {
    447       "al",  "cl",  "dl",   "bl",   "spl",  "bpl",  "sil",  "dil",
    448       "r8b", "r9b", "r10b", "r11b", "r12b", "r13b", "r14b", "r15b",
    449   };
    450   static const char* rh8[4] = {"ah", "ch", "dh", "bh"};
    451   reg &= 15u;
    452   if (width_bytes == 8) return r64[reg];
    453   if (width_bytes == 4) return r32[reg];
    454   if (width_bytes == 2) return r16[reg];
    455   if (!has_rex && reg >= 4u && reg <= 7u) return rh8[reg - 4u];
    456   return r8[reg];
    457 }
    458 
    459 static const char* xmm_name(u32 reg) {
    460   static const char* x[16] = {
    461       "xmm0", "xmm1", "xmm2",  "xmm3",  "xmm4",  "xmm5",  "xmm6",  "xmm7",
    462       "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15",
    463   };
    464   return x[reg & 15u];
    465 }
    466 
    467 static void put_reg(StrBuf* sb, u32 reg, u32 width) {
    468   strbuf_putc(sb, '%');
    469   strbuf_puts(sb, reg_name(reg, width, 1));
    470 }
    471 static void put_reg_ctx(StrBuf* sb, u32 reg, u32 width, int has_rex) {
    472   strbuf_putc(sb, '%');
    473   strbuf_puts(sb, reg_name(reg, width, has_rex));
    474 }
    475 static void put_xmm(StrBuf* sb, u32 reg) {
    476   strbuf_putc(sb, '%');
    477   strbuf_puts(sb, xmm_name(reg));
    478 }
    479 static void put_imm(StrBuf* sb, i64 imm) {
    480   strbuf_putc(sb, '$');
    481   strbuf_put_i64(sb, imm);
    482 }
    483 
    484 /* Read a signed displacement of n bytes (1 or 4). Returns 1 on success. */
    485 static int read_disp(const u8* bytes, u32 len, u32 off, u32 n, i32* out) {
    486   if (off + n > len) return 0;
    487   if (n == 1u) {
    488     *out = (i32)(i8)bytes[off];
    489   } else if (n == 4u) {
    490     *out = (i32)rd_u32_le(bytes + off);
    491   } else {
    492     *out = 0;
    493   }
    494   return 1;
    495 }
    496 
    497 /* Decode a ModR/M memory operand starting at bytes[off]. Returns number of
    498  * extra bytes consumed (ModR/M + SIB? + disp?), or (u32)-1 on truncation.
    499  * The ModR/M byte itself is bytes[off]; caller has already read mod/reg/rm.
    500  * `disp_out` and `base_out` describe what to print. */
    501 typedef struct DecodedMem {
    502   u32 base;
    503   u32 index; /* SIB index register (valid when has_index) */
    504   u32 scale; /* SIB scale as the literal 1/2/4/8 (valid when has_index) */
    505   i32 disp;
    506   int has_base;
    507   int has_index; /* a SIB index register is present */
    508   int rip_relative;
    509   u32 bytes_used;
    510 } DecodedMem;
    511 
    512 static u32 decode_mem(const u8* bytes, u32 len, u32 off, X64DecodeCtx ctx,
    513                       u32 mod, u32 rm_low, DecodedMem* out) {
    514   out->base = 0;
    515   out->index = 0;
    516   out->scale = 1;
    517   out->disp = 0;
    518   out->has_base = 1;
    519   out->has_index = 0;
    520   out->rip_relative = 0;
    521   out->bytes_used = 0;
    522   if (mod == 3u) return 0; /* caller handles reg-form */
    523   /* SIB-required form: r/m=100. */
    524   if (rm_low == 4u) {
    525     if (off >= len) return (u32)-1;
    526     u8 s = bytes[off];
    527     u32 sib_base = (s & 7u) | ((u32)ctx.rex_b << 3);
    528     u32 sib_index = ((s >> 3) & 7u) | ((u32)ctx.rex_x << 3);
    529     u32 used = 1;
    530     /* SIB index = 4 (RSP) with REX.X=0 encodes "no index". */
    531     if (sib_index != 4u) {
    532       out->has_index = 1;
    533       out->index = sib_index;
    534       out->scale = 1u << (s >> 6);
    535     }
    536     if (mod == 0u && (s & 7u) == 5u) {
    537       /* mod=00, base=101: disp32 with no base — either a label-table
    538        * disp32 (no index) or an indexed `[index*scale + disp32]`. */
    539       i32 d = 0;
    540       if (!read_disp(bytes, len, off + used, 4, &d)) return (u32)-1;
    541       used += 4;
    542       out->disp = d;
    543       out->has_base = 0;
    544       out->bytes_used = used;
    545       return used;
    546     }
    547     if (mod == 1u) {
    548       i32 d = 0;
    549       if (!read_disp(bytes, len, off + used, 1, &d)) return (u32)-1;
    550       used += 1;
    551       out->disp = d;
    552     } else if (mod == 2u) {
    553       i32 d = 0;
    554       if (!read_disp(bytes, len, off + used, 4, &d)) return (u32)-1;
    555       used += 4;
    556       out->disp = d;
    557     }
    558     out->base = sib_base;
    559     out->bytes_used = used;
    560     return used;
    561   }
    562   /* Non-SIB form. */
    563   if (mod == 0u && rm_low == 5u) {
    564     /* RIP-relative disp32. */
    565     i32 d = 0;
    566     if (!read_disp(bytes, len, off, 4, &d)) return (u32)-1;
    567     out->disp = d;
    568     out->rip_relative = 1;
    569     out->bytes_used = 4;
    570     return 4;
    571   }
    572   u32 base = rm_low | ((u32)ctx.rex_b << 3);
    573   out->base = base;
    574   if (mod == 1u) {
    575     i32 d = 0;
    576     if (!read_disp(bytes, len, off, 1, &d)) return (u32)-1;
    577     out->disp = d;
    578     out->bytes_used = 1;
    579     return 1;
    580   }
    581   if (mod == 2u) {
    582     i32 d = 0;
    583     if (!read_disp(bytes, len, off, 4, &d)) return (u32)-1;
    584     out->disp = d;
    585     out->bytes_used = 4;
    586     return 4;
    587   }
    588   /* mod == 0u with rm != 5,4 → [reg], no disp. */
    589   return 0;
    590 }
    591 
    592 static void put_mem(StrBuf* sb, const DecodedMem* m) {
    593   if (m->disp != 0 || (!m->has_base && !m->rip_relative)) {
    594     strbuf_put_i64(sb, (i64)m->disp);
    595   }
    596   if (m->rip_relative) {
    597     strbuf_puts(sb, "(%rip)");
    598   } else if (m->has_base || m->has_index) {
    599     /* `(base)`, `(base,index,scale)`, or the base-less `(,index,scale)`. */
    600     strbuf_putc(sb, '(');
    601     if (m->has_base) put_reg(sb, m->base, 8);
    602     if (m->has_index) {
    603       strbuf_putc(sb, ',');
    604       put_reg(sb, m->index, 8);
    605       strbuf_putc(sb, ',');
    606       strbuf_put_i64(sb, (i64)m->scale);
    607     }
    608     strbuf_putc(sb, ')');
    609   }
    610 }
    611 
    612 /* ====================================================================
    613  * Width derivation.
    614  * ==================================================================== */
    615 
    616 static u32 width_for(const X64InsnDesc* d, const X64DecodeCtx* ctx) {
    617   if (d->flags & X64_ASMFL_FORCE_W64) return 8u;
    618   if (d->flags & X64_ASMFL_BYTE) return 1u;
    619   if (d->flags & X64_ASMFL_W16) return 2u;
    620   if (d->flags & X64_ASMFL_W_FROM_REX) return ctx->rex_w ? 8u : 4u;
    621   if (d->leg_pfx == X64_PFX_66) return 2u;
    622   return 4u;
    623 }
    624 
    625 char x64_size_suffix_for(const X64InsnDesc* desc, const X64DecodeCtx* ctx) {
    626   switch ((X64Format)desc->fmt) {
    627     case X64_FMT_ALU_RR:
    628     case X64_FMT_MOV_RM_LOAD:
    629     case X64_FMT_ALU_RM_IMM8:
    630     case X64_FMT_ALU_RM_IMM32:
    631     case X64_FMT_IMUL_RR:
    632     case X64_FMT_IMUL_RRI:
    633     case X64_FMT_F7_RM:
    634     case X64_FMT_SHIFT_IMM:
    635     case X64_FMT_SHIFT_CL:
    636     case X64_FMT_BSWAP:
    637     case X64_FMT_BS:
    638     case X64_FMT_POPCNT:
    639     case X64_FMT_XADD_MEM:
    640     case X64_FMT_XCHG_MEM:
    641     case X64_FMT_CMPXCHG_MEM:
    642     case X64_FMT_MOV_RI:
    643       switch (width_for(desc, ctx)) {
    644         case 1:
    645           return 'b';
    646         case 2:
    647           return 'w';
    648         case 4:
    649           return 'l';
    650         case 8:
    651           return 'q';
    652       }
    653       return 0;
    654     default:
    655       return 0;
    656   }
    657 }
    658 
    659 /* ====================================================================
    660  * Per-format printers.
    661  * ==================================================================== */
    662 
    663 /* Decode a ModR/M with reg+r/m. Returns total bytes consumed by the
    664  * ModR/M + any SIB/disp. */
    665 typedef struct RegRm {
    666   u32 reg;    /* high bit from REX.R */
    667   u32 rm_low; /* low 3 bits */
    668   u32 mod;
    669   u32 bytes_after_modrm; /* SIB/disp bytes */
    670   DecodedMem mem;        /* valid iff mod != 3 */
    671 } RegRm;
    672 
    673 static int read_modrm(const u8* bytes, u32 len, u32 off, X64DecodeCtx ctx,
    674                       RegRm* rr) {
    675   if (off >= len) return 0;
    676   u8 mr = bytes[off];
    677   rr->mod = (mr >> 6) & 3u;
    678   rr->reg = ((mr >> 3) & 7u) | ((u32)ctx.rex_r << 3);
    679   rr->rm_low = mr & 7u;
    680   if (rr->mod == 3u) {
    681     rr->bytes_after_modrm = 0;
    682     memset(&rr->mem, 0, sizeof rr->mem);
    683     return 1;
    684   }
    685   u32 used =
    686       decode_mem(bytes, len, off + 1u, ctx, rr->mod, rr->rm_low, &rr->mem);
    687   if (used == (u32)-1) return 0;
    688   rr->bytes_after_modrm = used;
    689   return 1;
    690 }
    691 
    692 /* Print a ModR/M r/m operand at width `w`. */
    693 static void put_rm(StrBuf* sb, const RegRm* rr, X64DecodeCtx ctx, u32 w) {
    694   if (rr->mod == 3u) {
    695     u32 rm = rr->rm_low | ((u32)ctx.rex_b << 3);
    696     put_reg_ctx(sb, rm, w, ctx.has_rex);
    697   } else {
    698     put_mem(sb, &rr->mem);
    699   }
    700 }
    701 static void put_rm_xmm(StrBuf* sb, const RegRm* rr, X64DecodeCtx ctx) {
    702   if (rr->mod == 3u) {
    703     u32 rm = rr->rm_low | ((u32)ctx.rex_b << 3);
    704     put_xmm(sb, rm);
    705   } else {
    706     put_mem(sb, &rr->mem);
    707   }
    708 }
    709 
    710 static u32 print_nullary(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    711                          u32 len, const X64DecodeCtx* ctx) {
    712   (void)sb;
    713   (void)d;
    714   (void)bytes;
    715   (void)len;
    716   return ctx->opc_off + d->opc_len;
    717 }
    718 
    719 static u32 print_push_pop(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    720                           u32 len, const X64DecodeCtx* ctx) {
    721   (void)len;
    722   u32 reg = (bytes[ctx->opc_off] & 7u) | ((u32)ctx->rex_b << 3);
    723   put_reg(sb, reg, 8);
    724   (void)d;
    725   return ctx->opc_off + 1u;
    726 }
    727 
    728 static u32 print_mov_ri(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    729                         u32 len, const X64DecodeCtx* ctx) {
    730   (void)d;
    731   u32 reg = (bytes[ctx->opc_off] & 7u) | ((u32)ctx->rex_b << 3);
    732   u32 off = ctx->opc_off + 1u;
    733   if (ctx->rex_w) {
    734     if (off + 8u > len) return 0;
    735     put_imm(sb, (i64)rd_u64_le(bytes + off));
    736     off += 8u;
    737     strbuf_puts(sb, ", ");
    738     put_reg(sb, reg, 8);
    739   } else {
    740     if (off + 4u > len) return 0;
    741     put_imm(sb, (i64)(i32)rd_u32_le(bytes + off));
    742     off += 4u;
    743     strbuf_puts(sb, ", ");
    744     put_reg(sb, reg, 4);
    745   }
    746   return off;
    747 }
    748 
    749 static u32 print_alu_rr(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    750                         u32 len, const X64DecodeCtx* ctx) {
    751   /* op r/m, r (reg is the source). Width comes from width_for, which
    752    * honours the BYTE / W16 / W_FROM_REX flags on the descriptor. */
    753   u32 off = ctx->opc_off + d->opc_len;
    754   RegRm rr;
    755   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    756   u32 w = width_for(d, ctx);
    757   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
    758   strbuf_puts(sb, ", ");
    759   put_rm(sb, &rr, *ctx, w);
    760   return off + 1u + rr.bytes_after_modrm;
    761 }
    762 
    763 static u32 print_mov_rm_load(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    764                              u32 len, const X64DecodeCtx* ctx) {
    765   /* op r, r/m. */
    766   u32 off = ctx->opc_off + d->opc_len;
    767   RegRm rr;
    768   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    769   u32 w = width_for(d, ctx);
    770   if (d->opc[0] == 0x8Du) w = 8u; /* LEA always loads a 64-bit address */
    771   put_rm(sb, &rr, *ctx, w);
    772   strbuf_puts(sb, ", ");
    773   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
    774   return off + 1u + rr.bytes_after_modrm;
    775 }
    776 
    777 static u32 print_movzx_movsx(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    778                              u32 len, const X64DecodeCtx* ctx) {
    779   u32 off = ctx->opc_off + d->opc_len;
    780   RegRm rr;
    781   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    782   /* Source width = 1 for B6/BE, 2 for B7/BF. Destination width = 4 unless
    783    * REX.W (then 8). */
    784   u32 src_w = (d->opc[1] == 0xB7u || d->opc[1] == 0xBFu) ? 2u : 1u;
    785   u32 dst_w = ctx->rex_w ? 8u : 4u;
    786   put_rm(sb, &rr, *ctx, src_w);
    787   strbuf_puts(sb, ", ");
    788   put_reg_ctx(sb, rr.reg, dst_w, ctx->has_rex);
    789   return off + 1u + rr.bytes_after_modrm;
    790 }
    791 
    792 static u32 print_movsxd(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    793                         u32 len, const X64DecodeCtx* ctx) {
    794   u32 off = ctx->opc_off + d->opc_len;
    795   RegRm rr;
    796   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    797   put_rm(sb, &rr, *ctx, 4u);
    798   strbuf_puts(sb, ", ");
    799   put_reg_ctx(sb, rr.reg, 8u, ctx->has_rex);
    800   return off + 1u + rr.bytes_after_modrm;
    801 }
    802 
    803 static u32 print_alu_rm_imm(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    804                             u32 len, const X64DecodeCtx* ctx) {
    805   u32 off = ctx->opc_off + d->opc_len;
    806   RegRm rr;
    807   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    808   u32 used = 1u + rr.bytes_after_modrm;
    809   i64 imm = 0;
    810   if (d->fmt == X64_FMT_ALU_RM_IMM8) {
    811     if (off + used >= len) return 0;
    812     imm = (i64)(i8)bytes[off + used];
    813     used += 1u;
    814   } else {
    815     if (off + used + 3u >= len) return 0;
    816     imm = (i64)(i32)rd_u32_le(bytes + off + used);
    817     used += 4u;
    818   }
    819   u32 w = width_for(d, ctx);
    820   put_imm(sb, imm);
    821   strbuf_puts(sb, ", ");
    822   put_rm(sb, &rr, *ctx, w);
    823   return off + used;
    824 }
    825 
    826 static u32 print_imul_rr(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    827                          u32 len, const X64DecodeCtx* ctx) {
    828   u32 off = ctx->opc_off + d->opc_len;
    829   RegRm rr;
    830   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    831   u32 w = width_for(d, ctx);
    832   put_rm(sb, &rr, *ctx, w);
    833   strbuf_puts(sb, ", ");
    834   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
    835   return off + 1u + rr.bytes_after_modrm;
    836 }
    837 
    838 static u32 print_imul_rri(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    839                           u32 len, const X64DecodeCtx* ctx) {
    840   /* 69 /r imm32 (full) or 6B /r imm8 (sign-extended). */
    841   u32 off = ctx->opc_off + d->opc_len;
    842   RegRm rr;
    843   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    844   u32 used = 1u + rr.bytes_after_modrm;
    845   i64 imm = 0;
    846   u8 op = d->opc[0];
    847   if (op == 0x6Bu) {
    848     if (off + used >= len) return 0;
    849     imm = (i64)(i8)bytes[off + used];
    850     used += 1u;
    851   } else {
    852     if (off + used + 3u >= len) return 0;
    853     imm = (i64)(i32)rd_u32_le(bytes + off + used);
    854     used += 4u;
    855   }
    856   u32 w = width_for(d, ctx);
    857   put_imm(sb, imm);
    858   strbuf_puts(sb, ", ");
    859   put_rm(sb, &rr, *ctx, w);
    860   strbuf_puts(sb, ", ");
    861   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
    862   return off + used;
    863 }
    864 
    865 static u32 print_f7_rm(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    866                        u32 len, const X64DecodeCtx* ctx) {
    867   u32 off = ctx->opc_off + d->opc_len;
    868   RegRm rr;
    869   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    870   u32 w = width_for(d, ctx);
    871   put_rm(sb, &rr, *ctx, w);
    872   return off + 1u + rr.bytes_after_modrm;
    873 }
    874 
    875 static u32 print_shift_imm(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    876                            u32 len, const X64DecodeCtx* ctx) {
    877   u32 off = ctx->opc_off + d->opc_len;
    878   RegRm rr;
    879   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    880   u32 used = 1u + rr.bytes_after_modrm;
    881   if (off + used >= len) return 0;
    882   u8 imm = bytes[off + used];
    883   ++used;
    884   u32 w = width_for(d, ctx);
    885   put_imm(sb, (i64)imm);
    886   strbuf_puts(sb, ", ");
    887   put_rm(sb, &rr, *ctx, w);
    888   return off + used;
    889 }
    890 
    891 static u32 print_shift_cl(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    892                           u32 len, const X64DecodeCtx* ctx) {
    893   u32 off = ctx->opc_off + d->opc_len;
    894   RegRm rr;
    895   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    896   u32 w = width_for(d, ctx);
    897   strbuf_puts(sb, "%cl, ");
    898   put_rm(sb, &rr, *ctx, w);
    899   return off + 1u + rr.bytes_after_modrm;
    900 }
    901 
    902 static u32 print_jcc_rel32(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    903                            u32 len, const X64DecodeCtx* ctx, u64 vaddr) {
    904   u32 off = ctx->opc_off + d->opc_len;
    905   if (off + 4u > len) return 0;
    906   i32 rel = (i32)rd_u32_le(bytes + off);
    907   u64 tgt = vaddr + (u64)(off + 4u) + (u64)rel;
    908   /* Mnemonic suffix from condition nibble: caller wrote "j"; we append. */
    909   strbuf_putc(sb, ' ');
    910   strbuf_put_hex_u64(sb, tgt);
    911   return off + 4u;
    912 }
    913 
    914 static u32 print_jmp_call_rel32(StrBuf* sb, const X64InsnDesc* d,
    915                                 const u8* bytes, u32 len,
    916                                 const X64DecodeCtx* ctx, u64 vaddr) {
    917   u32 off = ctx->opc_off + d->opc_len;
    918   if (off + 4u > len) return 0;
    919   i32 rel = (i32)rd_u32_le(bytes + off);
    920   u64 tgt = vaddr + (u64)(off + 4u) + (u64)rel;
    921   strbuf_put_hex_u64(sb, tgt);
    922   return off + 4u;
    923 }
    924 
    925 static u32 print_br_rm(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    926                        u32 len, const X64DecodeCtx* ctx) {
    927   u32 off = ctx->opc_off + d->opc_len;
    928   RegRm rr;
    929   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    930   strbuf_putc(sb, '*');
    931   put_rm(sb, &rr, *ctx, 8u);
    932   return off + 1u + rr.bytes_after_modrm;
    933 }
    934 
    935 static u32 print_setcc(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    936                        u32 len, const X64DecodeCtx* ctx) {
    937   u32 off = ctx->opc_off + d->opc_len;
    938   RegRm rr;
    939   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    940   put_rm(sb, &rr, *ctx, 1u);
    941   (void)d;
    942   return off + 1u + rr.bytes_after_modrm;
    943 }
    944 
    945 static u32 print_cmovcc_rr(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    946                            u32 len, const X64DecodeCtx* ctx) {
    947   u32 off = ctx->opc_off + d->opc_len;
    948   RegRm rr;
    949   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    950   u32 w = width_for(d, ctx);
    951   put_rm(sb, &rr, *ctx, w);
    952   strbuf_puts(sb, ", ");
    953   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
    954   return off + 1u + rr.bytes_after_modrm;
    955 }
    956 
    957 static u32 print_bswap(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    958                        u32 len, const X64DecodeCtx* ctx) {
    959   (void)d;
    960   (void)len;
    961   u32 reg = (bytes[ctx->opc_off + 1u] & 7u) | ((u32)ctx->rex_b << 3);
    962   u32 w = ctx->rex_w ? 8u : 4u;
    963   put_reg_ctx(sb, reg, w, ctx->has_rex);
    964   return ctx->opc_off + 2u;
    965 }
    966 
    967 static u32 print_bs(StrBuf* sb, const X64InsnDesc* d, const u8* bytes, u32 len,
    968                     const X64DecodeCtx* ctx) {
    969   /* dst = bsr/bsf(src). Operand order in AT&T is "src, dst". */
    970   u32 off = ctx->opc_off + d->opc_len;
    971   RegRm rr;
    972   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    973   u32 w = width_for(d, ctx);
    974   put_rm(sb, &rr, *ctx, w);
    975   strbuf_puts(sb, ", ");
    976   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
    977   return off + 1u + rr.bytes_after_modrm;
    978 }
    979 
    980 static u32 print_xmm_rr(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
    981                         u32 len, const X64DecodeCtx* ctx) {
    982   u32 off = ctx->opc_off + d->opc_len;
    983   RegRm rr;
    984   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
    985   /* Operand classes/order by opcode (AT&T src, dst):
    986    *   2A CVTSI2*  : rm=GP(src),  reg=xmm(dst)  -> "rm_gp, reg_xmm"
    987    *   6E MOVD/Q   : rm=GP(src),  reg=xmm(dst)  -> "rm_gp, reg_xmm" (gpr->xmm)
    988    *   2C CVTT*2SI : rm=xmm(src), reg=GP(dst)   -> "rm_xmm, reg_gp"
    989    *   7E MOVD/Q   : reg=xmm(src), rm=GP(dst)   -> "reg_xmm, rm_gp" (reversed!)
    990    *   others      : both xmm                   -> "rm_xmm, reg_xmm"
    991    * GP width comes from REX.W (movd vs movq / 32- vs 64-bit operands). */
    992   u8 op = d->opc[1];
    993   u32 gp_w = ctx->rex_w ? 8u : 4u;
    994   if (op == 0x7Eu) {
    995     /* xmm -> r/m GPR: source is the reg-field xmm, dest is the r/m GPR. */
    996     put_xmm(sb, rr.reg);
    997     strbuf_puts(sb, ", ");
    998     put_rm(sb, &rr, *ctx, gp_w);
    999     return off + 1u + rr.bytes_after_modrm;
   1000   }
   1001   /* Store-direction XMM moves (MOVSD/MOVSS/MOVUPS 0x11, MOVAPS 0x29): the
   1002    * reg-field xmm is the SOURCE and the r/m (memory or xmm) is the
   1003    * DESTINATION — AT&T order `reg_xmm, rm`. Without this the disassembler
   1004    * prints them in load order, so re-assembly flips the data direction. */
   1005   if (op == 0x11u || op == 0x29u) {
   1006     put_xmm(sb, rr.reg);
   1007     strbuf_puts(sb, ", ");
   1008     put_rm_xmm(sb, &rr, *ctx);
   1009     return off + 1u + rr.bytes_after_modrm;
   1010   }
   1011   {
   1012     int dst_is_gp = (op == 0x2Cu);                /* CVTTSD/SS2SI */
   1013     int src_is_gp = (op == 0x2Au || op == 0x6Eu); /* CVTSI2*, MOVD/Q g->x */
   1014     if (src_is_gp) {
   1015       put_rm(sb, &rr, *ctx, gp_w);
   1016     } else {
   1017       put_rm_xmm(sb, &rr, *ctx);
   1018     }
   1019     strbuf_puts(sb, ", ");
   1020     if (dst_is_gp) {
   1021       put_reg_ctx(sb, rr.reg, gp_w, ctx->has_rex);
   1022     } else {
   1023       put_xmm(sb, rr.reg);
   1024     }
   1025   }
   1026   return off + 1u + rr.bytes_after_modrm;
   1027 }
   1028 
   1029 static u32 print_xadd_mem(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
   1030                           u32 len, const X64DecodeCtx* ctx) {
   1031   /* XADD r/m, r — source is the reg, destination is r/m. */
   1032   u32 off = ctx->opc_off + d->opc_len;
   1033   RegRm rr;
   1034   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
   1035   u32 w = width_for(d, ctx);
   1036   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
   1037   strbuf_puts(sb, ", ");
   1038   put_rm(sb, &rr, *ctx, w);
   1039   return off + 1u + rr.bytes_after_modrm;
   1040 }
   1041 
   1042 static u32 print_xchg_mem(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
   1043                           u32 len, const X64DecodeCtx* ctx) {
   1044   u32 off = ctx->opc_off + d->opc_len;
   1045   RegRm rr;
   1046   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
   1047   u32 w = width_for(d, ctx);
   1048   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
   1049   strbuf_puts(sb, ", ");
   1050   put_rm(sb, &rr, *ctx, w);
   1051   return off + 1u + rr.bytes_after_modrm;
   1052 }
   1053 
   1054 static u32 print_cmpxchg_mem(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
   1055                              u32 len, const X64DecodeCtx* ctx) {
   1056   /* CMPXCHG r/m, r — implicit RAX is the comparand; not shown. */
   1057   u32 off = ctx->opc_off + d->opc_len;
   1058   RegRm rr;
   1059   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
   1060   u32 w = width_for(d, ctx);
   1061   put_reg_ctx(sb, rr.reg, w, ctx->has_rex);
   1062   strbuf_puts(sb, ", ");
   1063   put_rm(sb, &rr, *ctx, w);
   1064   return off + 1u + rr.bytes_after_modrm;
   1065 }
   1066 
   1067 static u32 print_nop_multi(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
   1068                            u32 len, const X64DecodeCtx* ctx) {
   1069   (void)sb;
   1070   u32 off = ctx->opc_off + d->opc_len;
   1071   RegRm rr;
   1072   if (!read_modrm(bytes, len, off, *ctx, &rr)) return 0;
   1073   return off + 1u + rr.bytes_after_modrm;
   1074 }
   1075 
   1076 /* ====================================================================
   1077  * Dispatch.
   1078  * ==================================================================== */
   1079 
   1080 u32 x64_print_operands(StrBuf* sb, const X64InsnDesc* d, const u8* bytes,
   1081                        u32 len, const X64DecodeCtx* ctx, u64 vaddr) {
   1082   switch ((X64Format)d->fmt) {
   1083     case X64_FMT_NULLARY:
   1084       return print_nullary(sb, d, bytes, len, ctx);
   1085     case X64_FMT_NOP_MULTI:
   1086       return print_nop_multi(sb, d, bytes, len, ctx);
   1087     case X64_FMT_PUSH_POP:
   1088       return print_push_pop(sb, d, bytes, len, ctx);
   1089     case X64_FMT_MOV_RI:
   1090       return print_mov_ri(sb, d, bytes, len, ctx);
   1091     case X64_FMT_ALU_RR:
   1092       return print_alu_rr(sb, d, bytes, len, ctx);
   1093     case X64_FMT_MOV_RM_LOAD:
   1094       return print_mov_rm_load(sb, d, bytes, len, ctx);
   1095     case X64_FMT_MOVZX_MOVSX:
   1096       return print_movzx_movsx(sb, d, bytes, len, ctx);
   1097     case X64_FMT_MOVSXD:
   1098       return print_movsxd(sb, d, bytes, len, ctx);
   1099     case X64_FMT_ALU_RM_IMM8:
   1100     case X64_FMT_ALU_RM_IMM32:
   1101       return print_alu_rm_imm(sb, d, bytes, len, ctx);
   1102     case X64_FMT_IMUL_RR:
   1103       return print_imul_rr(sb, d, bytes, len, ctx);
   1104     case X64_FMT_IMUL_RRI:
   1105       return print_imul_rri(sb, d, bytes, len, ctx);
   1106     case X64_FMT_F7_RM:
   1107       return print_f7_rm(sb, d, bytes, len, ctx);
   1108     case X64_FMT_SHIFT_IMM:
   1109       return print_shift_imm(sb, d, bytes, len, ctx);
   1110     case X64_FMT_SHIFT_CL:
   1111       return print_shift_cl(sb, d, bytes, len, ctx);
   1112     case X64_FMT_JCC_REL32:
   1113       return print_jcc_rel32(sb, d, bytes, len, ctx, vaddr);
   1114     case X64_FMT_JMP_REL32:
   1115     case X64_FMT_CALL_REL32:
   1116       return print_jmp_call_rel32(sb, d, bytes, len, ctx, vaddr);
   1117     case X64_FMT_BR_RM:
   1118       return print_br_rm(sb, d, bytes, len, ctx);
   1119     case X64_FMT_SETCC_RM:
   1120       return print_setcc(sb, d, bytes, len, ctx);
   1121     case X64_FMT_CMOVCC_RR:
   1122       return print_cmovcc_rr(sb, d, bytes, len, ctx);
   1123     case X64_FMT_BSWAP:
   1124       return print_bswap(sb, d, bytes, len, ctx);
   1125     case X64_FMT_BS:
   1126       return print_bs(sb, d, bytes, len, ctx);
   1127     case X64_FMT_POPCNT:
   1128       return print_bs(sb, d, bytes, len, ctx); /* same shape */
   1129     case X64_FMT_SSE_RR:
   1130     case X64_FMT_SSE_LOAD:
   1131     case X64_FMT_SSE_STORE:
   1132       return print_xmm_rr(sb, d, bytes, len, ctx);
   1133     case X64_FMT_XADD_MEM:
   1134       return print_xadd_mem(sb, d, bytes, len, ctx);
   1135     case X64_FMT_XCHG_MEM:
   1136       return print_xchg_mem(sb, d, bytes, len, ctx);
   1137     case X64_FMT_CMPXCHG_MEM:
   1138       return print_cmpxchg_mem(sb, d, bytes, len, ctx);
   1139     case X64_FMT_RAW_BYTE:
   1140       return 0;
   1141   }
   1142   return 0;
   1143 }
   1144 
   1145 /* Resolve the condition nibble for Jcc/SETcc/CMOVcc to its AT&T mnemonic
   1146  * suffix. Used by the disassembler to spell j → "je", set → "sete", etc. */
   1147 const char* x64_cc_name(u8 cc) { return g_cc_name[cc & 0xFu]; }