code wiki / (root) / nxasm_x86_enc.nx

nxasm_x86_enc.nx source

↩ module page · 1022 lines · 42301 B

1// nxasm_x86_enc.nx -- x86_64 instruction encoder + minimal ELF writer, 2// written in NishiLang. The x86_64 sibling of asm_enc.nx (RV64). 3// 4// First brick of the SOVEREIGN x86_64 backend: replaces GNU `as` + `ld` 5// on the output path. Pure bit-manipulation -- no syscalls, no file 6// I/O here, so it stays architecture-neutral and the encoder KAT runs 7// under any lane. Live emit (write the ELF to a fd) is composed in the 8// test/driver via the syscall layer. 9// 10// Scope (this iteration): the exact constructs needed to emit a 11// runnable static ELF -- enough to prove the whole sovereign chain 12// end-to-end with an exit(N) binary. The full 29-mnemonic AT&T subset 13// nxc2 emits (movq/movabsq/add/sub/and/or/cmp/test/imul/idiv/cqo/shl/ 14// sar/lea/call/jmp/jcc/setcc/movzbq/...) lands incrementally on this 15// same REX/ModRM/SIB foundation, each gated by a byte-exact KAT vs the 16// objdump oracle (Wheeler/benchmark reference only -- NEVER in our 17// output path). 18// 19// Encoding model (Intel SDM Vol.2): 20// REX prefix 0100 WRXB (0x40 base; W=64-bit, B=reg ext bit3) 21// ModRM mod(2) reg(3) rm(3) 22// immediates little-endian 23// 24// genealogy_id: intel_sdm_vol2 + amd_apm + osdev_elf (Wheeler refs) 25// lineage_id: nishi_sovereign_x86_64_backend_m1 26// license_tier: ORIGINAL 27 28// ---- x86_64 general registers (encoding numbers) ---- 29const X86_RAX: i64 = 0 30const X86_RCX: i64 = 1 31const X86_RDX: i64 = 2 32const X86_RBX: i64 = 3 33const X86_RSP: i64 = 4 34const X86_RBP: i64 = 5 35const X86_RSI: i64 = 6 36const X86_RDI: i64 = 7 37const X86_R8: i64 = 8 38const X86_R9: i64 = 9 39const X86_R10: i64 = 10 40const X86_R11: i64 = 11 41const X86_R12: i64 = 12 42const X86_R13: i64 = 13 43const X86_R14: i64 = 14 44const X86_R15: i64 = 15 45 46// ---- ELF constants ---- 47const EM_X86_64: i64 = 62 48const X86_BASE: i64 = 0x400000 // conventional static ET_EXEC base 49const X86_HDRLEN: i64 = 120 // 64 (ehdr) + 56 (one phdr) 50 51// ---- little-endian buffer writers (return new offset) ---- 52func x86_put_u16le(out: *u8, o: i64, v: i64) -> i64 { 53 out[o] = v & 0xff 54 out[o + 1] = (v >> 8) & 0xff 55 return o + 2 56} 57 58func x86_put_u32le(out: *u8, o: i64, v: i64) -> i64 { 59 out[o] = v & 0xff 60 out[o + 1] = (v >> 8) & 0xff 61 out[o + 2] = (v >> 16) & 0xff 62 out[o + 3] = (v >> 24) & 0xff 63 return o + 4 64} 65 66func x86_put_u64le(out: *u8, o: i64, v: i64) -> i64 { 67 out[o] = v & 0xff 68 out[o + 1] = (v >> 8) & 0xff 69 out[o + 2] = (v >> 16) & 0xff 70 out[o + 3] = (v >> 24) & 0xff 71 out[o + 4] = (v >> 32) & 0xff 72 out[o + 5] = (v >> 40) & 0xff 73 out[o + 6] = (v >> 48) & 0xff 74 out[o + 7] = (v >> 56) & 0xff 75 return o + 8 76} 77 78// REX.W prefix, optionally extending reg (bit3 -> REX.B) and a second 79// reg field (bit3 -> REX.R). 0x48 = 0100 1000 (W set). 80func x86_rex_w(reg_field: i64, rm_field: i64) -> i64 { 81 var rex: i64 = 0x48 82 if (reg_field & 8) != 0 { rex = rex | 0x04 } // REX.R 83 if (rm_field & 8) != 0 { rex = rex | 0x01 } // REX.B 84 return rex 85} 86 87// ModRM byte: mod(2) | reg(3) | rm(3). 88func x86_modrm(mod: i64, reg: i64, rm: i64) -> i64 { 89 return ((mod & 3) << 6) | ((reg & 7) << 3) | (rm & 7) 90} 91 92// SSE/SHA-NI register-register: [pfx?] [REX?] 0F [esc?] opc ModRM(11,reg,rm). 93// pfx = 0x66/0xF3/0xF2 or 0 (none); esc = 0x38/0x3A or 0 (none). reg/rm = xmm 0..15. 94// REX (with R/B for xmm8-15) is emitted only when needed, AFTER any mandatory prefix. 95// This single primitive covers sha256rnds2/msg1/msg2 (esc=0x38), pshufb (66,0x38), 96// paddd/pxor/punpck*/movdqa/movdqu (66 or F3, no esc) -- the SHA-256 SHA-NI core. 97func x86_sse_rr(out: *u8, o: i64, pfx: i64, esc: i64, opc: i64, reg: i64, rm: i64) -> i64 { 98 var p: i64 = o 99 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 } 100 var rex: i64 = 0x40 101 var need: i64 = 0 102 if reg >= 8 { rex = rex | 4; need = 1 } 103 if rm >= 8 { rex = rex | 1; need = 1 } 104 if need == 1 { out[p] = rex & 0xff; p = p + 1 } 105 out[p] = 0x0f; p = p + 1 106 if esc != 0 { out[p] = esc & 0xff; p = p + 1 } 107 out[p] = opc & 0xff; p = p + 1 108 out[p] = x86_modrm(3, reg, rm); p = p + 1 109 return p 110} 111 112// SSE SCALAR reg-reg WITH REX.W: the GPR<->xmm converts (cvtsi2ss r64->xmm, 113// cvtss2si/cvttss2si xmm->r64) operate on a 64-bit GPR, so REX.W is mandatory 114// and REX is ALWAYS emitted (after any mandatory prefix, before 0F). Same 115// ModRM(11,reg,rm) shape as x86_sse_rr; reg/rm still extend to 8..15 via 116// REX.R/REX.B. This is the ONLY new encoder hardware f32 (R1) needs -- the 117// scalar-float ALU ops (addss/subss/mulss/divss/sqrtss) reuse x86_sse_rr. 118// ---- AVX/VEX encoders (the 8-wide packed lever toward physics) ----------------- 119// 2-byte VEX (C5) reg-reg: 0F-map ops on ymm/xmm, NDS 3-operand form. 120// dst = ModRM.reg, src2 = ModRM.rm, src1 = VEX.vvvv (inverted). L=1 -> 256-bit ymm. 121// pp: 0=none(packed-single 0F), 1=66, 2=F3, 3=F2. R extends dst to 8..15; src2 must be 0..7 122// (2-byte VEX has no B bit -- use x86_vex3_rr for src2>=8). e.g. vmulps ymm0,ymm1,ymm2 = C5 F4 59 C2. 123func x86_vex2_rr(out: *u8, o: i64, opc: i64, dst: i64, src1: i64, src2: i64, l: i64, pp: i64) -> i64 { 124 var p: i64 = o 125 out[p] = 0xc5; p = p + 1 126 var rbit: i64 = 1 127 if dst >= 8 { rbit = 0 } 128 let vvvv: i64 = (15 - (src1 & 15)) & 15 129 out[p] = (((rbit & 1) << 7) | ((vvvv & 15) << 3) | ((l & 1) << 2) | (pp & 3)) & 0xff; p = p + 1 130 out[p] = opc & 0xff; p = p + 1 131 out[p] = x86_modrm(3, dst & 7, src2 & 7); p = p + 1 132 return p 133} 134// 3-byte VEX (C4) reg-reg: needed for the 0F38/0F3A maps (FMA) and src2>=8. 135// mmmmm: 1=0F, 2=0F38, 3=0F3A. w: VEX.W. e.g. vfmadd231ps ymm0,ymm1,ymm2 = C4 E2 75 B8 C2. 136func x86_vex3_rr(out: *u8, o: i64, mmmmm: i64, w: i64, opc: i64, dst: i64, src1: i64, src2: i64, l: i64, pp: i64) -> i64 { 137 var p: i64 = o 138 out[p] = 0xc4; p = p + 1 139 var rbit: i64 = 1 140 if dst >= 8 { rbit = 0 } 141 var bbit: i64 = 1 142 if src2 >= 8 { bbit = 0 } 143 out[p] = (((rbit & 1) << 7) | (1 << 6) | ((bbit & 1) << 5) | (mmmmm & 31)) & 0xff; p = p + 1 144 let vvvv: i64 = (15 - (src1 & 15)) & 15 145 out[p] = (((w & 1) << 7) | ((vvvv & 15) << 3) | ((l & 1) << 2) | (pp & 3)) & 0xff; p = p + 1 146 out[p] = opc & 0xff; p = p + 1 147 out[p] = x86_modrm(3, dst & 7, src2 & 7); p = p + 1 148 return p 149} 150func x86_sse_rr_w(out: *u8, o: i64, pfx: i64, esc: i64, opc: i64, reg: i64, rm: i64) -> i64 { 151 var p: i64 = o 152 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 } 153 var rex: i64 = 0x48 154 if reg >= 8 { rex = rex | 4 } 155 if rm >= 8 { rex = rex | 1 } 156 out[p] = rex & 0xff; p = p + 1 157 out[p] = 0x0f; p = p + 1 158 if esc != 0 { out[p] = esc & 0xff; p = p + 1 } 159 out[p] = opc & 0xff; p = p + 1 160 out[p] = x86_modrm(3, reg, rm); p = p + 1 161 return p 162} 163 164// SSE register<->MEMORY: [pfx?] [REX?] 0F opc ModRM(mem)+SIB+disp. The 165// memory twin of x86_sse_rr -- the follow-on named at nxasm_x86.nx:454. 166// Encodes xmm<->m128 load/store (movdqu/movdqa mem forms): reg = the xmm 167// (0..15) goes in ModRM.reg; base/disp is the `disp(%base)` operand, run 168// through the proven x86_mem_operand (ModRM mod/disp8/disp32 + rsp/r12 SIB). 169// REX.R extends the xmm (xmm8-15); REX.B extends the base (r8-15); both 170// emitted AFTER the mandatory 66/F3 prefix per Intel SDM Vol.2 prefix order. 171// This is the load/store leg every SIMD kernel needs to move vectors in and 172// out of memory (ChaCha state, FNet tiles); reg-reg packed ALU already exists. 173func x86_sse_mem(out: *u8, o: i64, pfx: i64, opc: i64, reg: i64, base: i64, disp: i64) -> i64 { 174 var p: i64 = o 175 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 } 176 var rex: i64 = 0x40 177 var need: i64 = 0 178 if reg >= 8 { rex = rex | 4; need = 1 } // REX.R (xmm8-15) 179 if base >= 8 { rex = rex | 1; need = 1 } // REX.B (r8-15 base) 180 if need == 1 { out[p] = rex & 0xff; p = p + 1 } 181 out[p] = 0x0f; p = p + 1 182 out[p] = opc & 0xff; p = p + 1 183 return x86_mem_operand(out, p, reg, base, disp) 184} 185 186// SSE packed shift-by-IMMEDIATE: 66 [REX.B?] 0F opc /ext ib. The ModRM.reg 187// field is the opcode EXTENSION (not a register): pslld=/6 psrld=/2 on opc 188// 0x72 (dword), psllq=/6 psrlq=/2 on opc 0x73 (qword). rm = the xmm being 189// shifted (REX.B extends it to xmm8-15; there is no REX.R since reg is /ext). 190// This is the rotate primitive: rotl32(x,n) = (pslld x,n) OR (psrld x,32-n), 191// the inner loop of ChaCha/BLAKE and every ARX cipher. 192func x86_sse_shift_imm(out: *u8, o: i64, opc: i64, ext: i64, xmm: i64, imm8: i64) -> i64 { 193 var p: i64 = o 194 out[p] = 0x66; p = p + 1 195 if xmm >= 8 { out[p] = 0x41; p = p + 1 } // REX.B extends rm (xmm8-15) 196 out[p] = 0x0f; p = p + 1 197 out[p] = opc & 0xff; p = p + 1 198 out[p] = x86_modrm(3, ext, xmm); p = p + 1 199 out[p] = imm8 & 0xff; p = p + 1 200 return p 201} 202 203// SSE reg-reg-with-IMMEDIATE: [pfx?] [REX?] 0F opc ModRM(11,reg,rm) ib. 204// x86_sse_rr + a trailing imm8. Covers pshufd (66 0F 70 /r ib), pshuflw 205// (F2), pshufhw (F3), pshufw, shufps, etc. -- the 3-operand `$imm,%src,%dst` 206// shuffles. reg = dst (ModRM.reg), rm = src (ModRM.rm), imm8 = the control. 207func x86_sse_rri(out: *u8, o: i64, pfx: i64, opc: i64, reg: i64, rm: i64, imm8: i64) -> i64 { 208 var p: i64 = o 209 if pfx != 0 { out[p] = pfx & 0xff; p = p + 1 } 210 var rex: i64 = 0x40 211 var need: i64 = 0 212 if reg >= 8 { rex = rex | 4; need = 1 } 213 if rm >= 8 { rex = rex | 1; need = 1 } 214 if need == 1 { out[p] = rex & 0xff; p = p + 1 } 215 out[p] = 0x0f; p = p + 1 216 out[p] = opc & 0xff; p = p + 1 217 out[p] = x86_modrm(3, reg, rm); p = p + 1 218 out[p] = imm8 & 0xff; p = p + 1 219 return p 220} 221 222// ---- AVX / AVX2 VEX-prefix encoders (256-bit ymm, 3-byte C4 form) ---- 223// The 3-byte VEX always-correct form (encodes all 16 ymm + every map/pp/W); 224// the shorter 2-byte C5 form is a later size optimization. Intel SDM Vol.2: 225// C4 | R~X~B~ mmmmm | W vvvv~ L pp | opcode | ModRM [SIB][disp][imm] 226// R~/X~/B~ and vvvv~ are stored INVERTED. pp: 0=none 1=66 2=F3 3=F2. 227// mmmmm: 1=0F 2=0F38 3=0F3A. L: 0=128(xmm) 1=256(ymm). 228 229// 3-operand reg ALU `<op> %src2,%src1,%dst` (AT&T): dst=ModRM.reg, src2=ModRM.rm, 230// src1=VEX.vvvv. Covers vpaddd/vpsubd/vpxor/vpand/vpor/vpaddq/... (the AVX2 NDS form). 231func x86_vex_rrr(out: *u8, o: i64, pp: i64, mmmmm: i64, w: i64, vexL: i64, opc: i64, dst: i64, src1: i64, src2: i64) -> i64 { 232 var p: i64 = o 233 out[p] = 0xc4; p = p + 1 234 var b1: i64 = mmmmm & 0x1f 235 if dst < 8 { b1 = b1 | 0x80 } // R~ (inverted REX.R from ModRM.reg) 236 b1 = b1 | 0x40 // X~ = 1 (no index in reg form) 237 if src2 < 8 { b1 = b1 | 0x20 } // B~ (inverted REX.B from ModRM.rm) 238 out[p] = b1; p = p + 1 239 var b2: i64 = (((0 - 1) - src1) & 0xf) << 3 // vvvv~ = ~src1 (4-bit), 1st source 240 if w != 0 { b2 = b2 | 0x80 } 241 if vexL != 0 { b2 = b2 | 0x04 } 242 b2 = b2 | (pp & 3) 243 out[p] = b2; p = p + 1 244 out[p] = opc & 0xff; p = p + 1 245 out[p] = x86_modrm(3, dst, src2); p = p + 1 246 return p 247} 248 249// 2-operand reg<->MEMORY `<op>` with no 1st-source (vvvv=1111): vmovdqu load/store. 250// reg = ModRM.reg (the ymm), base/disp = the memory operand via x86_mem_operand. 251func x86_vex_rm(out: *u8, o: i64, pp: i64, mmmmm: i64, w: i64, vexL: i64, opc: i64, reg: i64, base: i64, disp: i64) -> i64 { 252 var p: i64 = o 253 out[p] = 0xc4; p = p + 1 254 var b1: i64 = mmmmm & 0x1f 255 if reg < 8 { b1 = b1 | 0x80 } // R~ from ModRM.reg 256 b1 = b1 | 0x40 // X~ = 1 (no index) 257 if base < 8 { b1 = b1 | 0x20 } // B~ from base 258 out[p] = b1; p = p + 1 259 var b2: i64 = 0x78 // vvvv~ = 1111 (unused), then OR L/pp/W 260 if w != 0 { b2 = b2 | 0x80 } 261 if vexL != 0 { b2 = b2 | 0x04 } 262 b2 = b2 | (pp & 3) 263 out[p] = b2; p = p + 1 264 out[p] = opc & 0xff; p = p + 1 265 return x86_mem_operand(out, p, reg, base, disp) 266} 267 268// VEX shift-by-IMMEDIATE, NDD (dst in VEX.vvvv): VEX 66.0F <opc> /ext ib. The 269// non-destructive 3-operand form `<op> $imm,%src,%dst`: ModRM.reg = /ext (opcode 270// extension, REX.R unused), ModRM.rm = src, VEX.vvvv = dst. vpslld=/6 vpsrld=/2 271// vpsrad=/4 on 0x72(d); vpsllq=/6 vpsrlq=/2 on 0x73(q). The AVX2 ARX rotate leg: 272// rotl(v,n) = vpslld $n,v,t ; vpsrld $32-n,v,v ; vpor t,v,v (no movdqa, VEX-native). 273func x86_vex_shift_imm(out: *u8, o: i64, pp: i64, mmmmm: i64, vexL: i64, opc: i64, ext: i64, dst: i64, src: i64, imm8: i64) -> i64 { 274 var p: i64 = o 275 out[p] = 0xc4; p = p + 1 276 var b1: i64 = mmmmm & 0x1f 277 b1 = b1 | 0x80 // R~ = 1 (ModRM.reg is the /ext) 278 b1 = b1 | 0x40 // X~ = 1 279 if src < 8 { b1 = b1 | 0x20 } // B~ from src (ModRM.rm) 280 out[p] = b1; p = p + 1 281 var b2: i64 = (((0 - 1) - dst) & 0xf) << 3 // vvvv~ = ~dst 282 if vexL != 0 { b2 = b2 | 0x04 } 283 b2 = b2 | (pp & 3) 284 out[p] = b2; p = p + 1 285 out[p] = opc & 0xff; p = p + 1 286 out[p] = x86_modrm(3, ext, src); p = p + 1 287 out[p] = imm8 & 0xff; p = p + 1 288 return p 289} 290 291// VEX reg<-rm with IMMEDIATE, no 1st-source (vvvv=1111): `<op> $imm,%src,%dst`. 292// reg=dst(ModRM.reg), rm=src(ModRM.rm), imm8. vpshufd (66.0F 70 /r ib), vpshuflw 293// (F2), vpshufhw (F3), vpermq/vpermpd (66.0F3A W1). The per-128-lane dword shuffle 294// (ChaCha diagonalize across 2 blocks at once on ymm). 295func x86_vex_rmi(out: *u8, o: i64, pp: i64, mmmmm: i64, w: i64, vexL: i64, opc: i64, dst: i64, src: i64, imm8: i64) -> i64 { 296 var p: i64 = o 297 out[p] = 0xc4; p = p + 1 298 var b1: i64 = mmmmm & 0x1f 299 if dst < 8 { b1 = b1 | 0x80 } // R~ from dst 300 b1 = b1 | 0x40 // X~ = 1 301 if src < 8 { b1 = b1 | 0x20 } // B~ from src 302 out[p] = b1; p = p + 1 303 var b2: i64 = 0x78 // vvvv~ = 1111 304 if w != 0 { b2 = b2 | 0x80 } 305 if vexL != 0 { b2 = b2 | 0x04 } 306 b2 = b2 | (pp & 3) 307 out[p] = b2; p = p + 1 308 out[p] = opc & 0xff; p = p + 1 309 out[p] = x86_modrm(3, dst, src); p = p + 1 310 out[p] = imm8 & 0xff; p = p + 1 311 return p 312} 313 314// ---- instruction encoders (write into out at o, return new offset) ---- 315 316// movabsq $imm64, %reg -> REX.W B8+rd imm64 317func x86_movabs(out: *u8, o: i64, reg: i64, imm: i64) -> i64 { 318 var p: i64 = o 319 out[p] = 0x48 | ((reg >> 3) & 1) // REX.W (+REX.B if reg>=8) 320 p = p + 1 321 out[p] = 0xB8 + (reg & 7) 322 p = p + 1 323 return x86_put_u64le(out, p, imm) 324} 325 326// movq $imm32, %reg -> REX.W C7 /0 id (imm sign-extended to 64) 327func x86_mov_imm32(out: *u8, o: i64, reg: i64, imm: i64) -> i64 { 328 var p: i64 = o 329 out[p] = x86_rex_w(0, reg) 330 p = p + 1 331 out[p] = 0xC7 332 p = p + 1 333 out[p] = x86_modrm(3, 0, reg) 334 p = p + 1 335 return x86_put_u32le(out, p, imm) 336} 337 338// movq %src, %dst -> REX.W 89 /r (reg=src, rm=dst) 339func x86_mov_reg(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 340 var p: i64 = o 341 out[p] = x86_rex_w(src, dst) 342 p = p + 1 343 out[p] = 0x89 344 p = p + 1 345 out[p] = x86_modrm(3, src, dst) 346 return p + 1 347} 348 349// syscall -> 0F 05 350func x86_syscall(out: *u8, o: i64) -> i64 { 351 out[o] = 0x0F 352 out[o + 1] = 0x05 353 return o + 2 354} 355 356// rdtsc -> 0F 31 (read time-stamp counter into edx:eax; no operands). Added 2026-07-06 so the sovereign 357// assembler can build the cycle-measurement organs (nx_rdtsc_test / nx_linkqual_cycles) -- previously only GNU 358// `as` could, which forced nx_engineer's rdtsc gate onto a /bin/bash .sh wrapper. 359func x86_rdtsc(out: *u8, o: i64) -> i64 { 360 out[o] = 0x0F 361 out[o + 1] = 0x31 362 return o + 2 363} 364 365// ret -> C3 366func x86_ret(out: *u8, o: i64) -> i64 { 367 out[o] = 0xC3 368 return o + 1 369} 370 371// pushq %reg -> (REX.B if reg>=8) 50+rd 372func x86_push(out: *u8, o: i64, reg: i64) -> i64 { 373 var p: i64 = o 374 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 } 375 out[p] = 0x50 + (reg & 7) 376 return p + 1 377} 378 379// popq %reg -> (REX.B if reg>=8) 58+rd 380func x86_pop(out: *u8, o: i64, reg: i64) -> i64 { 381 var p: i64 = o 382 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 } 383 out[p] = 0x58 + (reg & 7) 384 return p + 1 385} 386 387// ---- ALU reg,reg opcodes (MR form: REX.W <op> /r ; reg=src, rm=dst) ---- 388const X86_OP_ADD: i64 = 0x01 389const X86_OP_SUB: i64 = 0x29 390const X86_OP_AND: i64 = 0x21 391const X86_OP_OR: i64 = 0x09 392const X86_OP_XOR: i64 = 0x31 393const X86_OP_CMP: i64 = 0x39 394const X86_OP_TEST: i64 = 0x85 395// ALU imm extension digits (REX.W 83 /ext ib | 81 /ext id) 396const X86_EXT_ADD: i64 = 0 397const X86_EXT_OR: i64 = 1 398const X86_EXT_AND: i64 = 4 399const X86_EXT_SUB: i64 = 5 400const X86_EXT_CMP: i64 = 7 401// shift extension digits (REX.W C1 /ext ib) 402const X86_EXT_SHL: i64 = 4 403const X86_EXT_SHR: i64 = 5 404const X86_EXT_SAR: i64 = 7 405// condition codes (jcc 0F 80+cc rel32 | setcc 0F 90+cc /0 | cmovcc 0F 40+cc /r) 406const X86_CC_E: i64 = 4 407const X86_CC_NE: i64 = 5 408const X86_CC_L: i64 = 0xC 409const X86_CC_LE: i64 = 0xE 410const X86_CC_G: i64 = 0xF 411const X86_CC_GE: i64 = 0xD 412const X86_CC_S: i64 = 8 // SF=1 (negative) -- the G22 bias-select 413const X86_CC_NS: i64 = 9 // SF=0 414 415// ALU reg,reg: `<op>q %src,%dst` -> REX.W <opcode> ModRM(11 src dst) 416func x86_alu_rr(out: *u8, o: i64, opcode: i64, dst: i64, src: i64) -> i64 { 417 var p: i64 = o 418 out[p] = x86_rex_w(src, dst); p = p + 1 419 out[p] = opcode; p = p + 1 420 out[p] = x86_modrm(3, src, dst); p = p + 1 421 return p 422} 423 424// ALU imm,reg: `<op>q $imm,%dst`. imm8 form (83 /ext ib) when it fits a 425// signed byte, else imm32 form (81 /ext id) sign-extended to 64. 426func x86_alu_imm(out: *u8, o: i64, ext: i64, dst: i64, imm: i64) -> i64 { 427 var p: i64 = o 428 out[p] = x86_rex_w(0, dst); p = p + 1 429 var short: i64 = 0 430 if imm >= -128 { if imm <= 127 { short = 1 } } 431 if short == 1 { 432 out[p] = 0x83; p = p + 1 433 out[p] = x86_modrm(3, ext, dst); p = p + 1 434 out[p] = imm & 0xff; p = p + 1 435 return p 436 } 437 out[p] = 0x81; p = p + 1 438 out[p] = x86_modrm(3, ext, dst); p = p + 1 439 return x86_put_u32le(out, p, imm) 440} 441 442// Emit ModRM(+SIB+disp) for a `disp(%base)` memory operand with the 443// other operand in reg_field. mod=01(disp8) if disp fits a signed byte 444// else mod=10(disp32); base&7==4 (rsp/r12) requires the SIB byte 0x24. 445// (base==rbp with disp=0 still uses disp8=0, matching gas.) 446func x86_mem_operand(out: *u8, o: i64, reg_field: i64, base: i64, disp: i64) -> i64 { 447 var p: i64 = o 448 var mod: i64 = 2 449 if disp >= -128 { if disp <= 127 { mod = 1 } } 450 out[p] = x86_modrm(mod, reg_field, base & 7); p = p + 1 451 if (base & 7) == 4 { out[p] = 0x24; p = p + 1 } // SIB: scale0 index=none base=rsp 452 if mod == 1 { out[p] = disp & 0xff; return p + 1 } 453 return x86_put_u32le(out, p, disp) 454} 455 456// movq disp(%base),%dst (load) -> REX.W 8B /r 457func x86_mov_load(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 458 var p: i64 = o 459 out[p] = x86_rex_w(dst, base); p = p + 1 460 out[p] = 0x8B; p = p + 1 461 return x86_mem_operand(out, p, dst, base, disp) 462} 463 464// movq %src,disp(%base) (store) -> REX.W 89 /r 465func x86_mov_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 { 466 var p: i64 = o 467 out[p] = x86_rex_w(src, base); p = p + 1 468 out[p] = 0x89; p = p + 1 469 return x86_mem_operand(out, p, src, base, disp) 470} 471 472// ---- SIB (%base,%index,scale) addressing (2026-07-15, for array indexing) ---- 473// REX.W + X(index) + R(reg) + B(base). The plain x86_rex_w has no X bit, so 474// indexed addressing needs this variant. 475func x86_rex_wx(reg_field: i64, index_field: i64, base_field: i64) -> i64 { 476 var rex: i64 = 0x48 477 if (reg_field & 8) != 0 { rex = rex | 0x04 } // REX.R 478 if (index_field & 8) != 0 { rex = rex | 0x02 } // REX.X 479 if (base_field & 8) != 0 { rex = rex | 0x01 } // REX.B 480 return rex 481} 482 483// scale VALUE {1,2,4,8} -> the 2-bit SIB scale field {0,1,2,3}. 484func x86_scale_log2(scale: i64) -> i64 { 485 if scale == 8 { return 3 } 486 if scale == 4 { return 2 } 487 if scale == 2 { return 1 } 488 return 0 489} 490 491// ModRM(rm=100 => SIB) + SIB(scale,index,base) + disp for (%base,%index,scale). 492// mod=00 (no disp) when disp==0 AND base&7 != 5 (rbp/r13 need disp8=0); else 493// disp8/disp32. index MUST NOT be rsp (4) -- the SIB index=100 means "none"; 494// the compiler never selects rsp as an index, and the parser rejects it below. 495func x86_mem_operand_sib(out: *u8, o: i64, reg_field: i64, base: i64, 496 index: i64, scale: i64, disp: i64) -> i64 { 497 var p: i64 = o 498 var mod: i64 = 2 499 if disp >= -128 { if disp <= 127 { mod = 1 } } 500 if disp == 0 { if (base & 7) != 5 { mod = 0 } } 501 out[p] = x86_modrm(mod, reg_field, 4); p = p + 1 // rm=100 => SIB follows 502 out[p] = ((x86_scale_log2(scale) & 3) << 6) | ((index & 7) << 3) | (base & 7); p = p + 1 503 if mod == 0 { return p } 504 if mod == 1 { out[p] = disp & 0xff; return p + 1 } 505 return x86_put_u32le(out, p, disp) 506} 507 508// movq disp(%base,%index,scale),%dst (load) -> REX.WX 8B /r SIB 509func x86_mov_load_sib(out: *u8, o: i64, dst: i64, base: i64, index: i64, scale: i64, disp: i64) -> i64 { 510 var p: i64 = o 511 out[p] = x86_rex_wx(dst, index, base); p = p + 1 512 out[p] = 0x8B; p = p + 1 513 return x86_mem_operand_sib(out, p, dst, base, index, scale, disp) 514} 515 516// movq %src,disp(%base,%index,scale) (store) -> REX.WX 89 /r SIB 517func x86_mov_store_sib(out: *u8, o: i64, base: i64, index: i64, scale: i64, disp: i64, src: i64) -> i64 { 518 var p: i64 = o 519 out[p] = x86_rex_wx(src, index, base); p = p + 1 520 out[p] = 0x89; p = p + 1 521 return x86_mem_operand_sib(out, p, src, base, index, scale, disp) 522} 523 524// movl (32-bit): store 89 /r, load 8B /r -- NO REX.W; REX only for r8d+. 525// Emitted by the compiler for i32 struct-field stores (e.g. pollfd.fd). 526func x86_movl_rex_if(out: *u8, o: i64, reg: i64, rm: i64) -> i64 { 527 if ((reg | rm) & 8) == 0 { return o } 528 var rex: i64 = 0x40 529 if (reg & 8) != 0 { rex = rex | 0x04 } 530 if (rm & 8) != 0 { rex = rex | 0x01 } 531 out[o] = rex 532 return o + 1 533} 534func x86_movl_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 { 535 var p: i64 = x86_movl_rex_if(out, o, src, base) 536 out[p] = 0x89; p = p + 1 537 return x86_mem_operand(out, p, src, base, disp) 538} 539func x86_movl_load(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 540 var p: i64 = x86_movl_rex_if(out, o, dst, base) 541 out[p] = 0x8B; p = p + 1 542 return x86_mem_operand(out, p, dst, base, disp) 543} 544// movl %src,%dst (reg-reg) -> [REX] 89 /r mod=3 545func x86_movl_rr(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 546 var p: i64 = x86_movl_rex_if(out, o, src, dst) 547 out[p] = 0x89; p = p + 1 548 out[p] = x86_modrm(3, src, dst); p = p + 1 549 return p 550} 551 552// leaq disp(%base),%dst -> REX.W 8D /r 553func x86_lea(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 554 var p: i64 = o 555 out[p] = x86_rex_w(dst, base); p = p + 1 556 out[p] = 0x8D; p = p + 1 557 return x86_mem_operand(out, p, dst, base, disp) 558} 559 560// leaq disp(%base,%index,scale),%dst -> REX.WRXB 8D /r SIB. 561// Same ModRM/SIB/disp shape as x86_mov_load_sib (8B), opcode 8D. The address 562// arithmetic (base + index*scale + disp) is COMPUTED into dst, no memory touch; 563// this is the lea-strength peephole target (c*n+d => leaq d(%n,%n,s) for 564// c in {2,3,5,9} with base==index==n). 565func x86_lea_sib(out: *u8, o: i64, dst: i64, base: i64, index: i64, scale: i64, disp: i64) -> i64 { 566 var p: i64 = o 567 out[p] = x86_rex_wx(dst, index, base); p = p + 1 568 out[p] = 0x8D; p = p + 1 569 return x86_mem_operand_sib(out, p, dst, base, index, scale, disp) 570} 571 572// leaq disp(%rip),%dst -> REX.W 8D /r, ModRM mod=00 reg=dst rm=101, disp32. 573// disp32 is relative to the END of this 7-byte instruction (RIP-relative). 574func x86_lea_rip(out: *u8, o: i64, dst: i64, disp32: i64) -> i64 { 575 var p: i64 = o 576 var rex: i64 = 0x48 577 if (dst & 8) != 0 { rex = rex | 0x04 } // REX.R 578 out[p] = rex; p = p + 1 579 out[p] = 0x8D; p = p + 1 580 out[p] = x86_modrm(0, dst, 5); p = p + 1 // mod=00 rm=101 => RIP-relative 581 return x86_put_u32le(out, p, disp32) 582} 583 584// imulq %src,%dst -> REX.W 0F AF /r (reg=dst, rm=src) 585func x86_imul_rr(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 586 var p: i64 = o 587 out[p] = x86_rex_w(dst, src); p = p + 1 588 out[p] = 0x0F; p = p + 1 589 out[p] = 0xAF; p = p + 1 590 out[p] = x86_modrm(3, dst, src); p = p + 1 591 return p 592} 593 594// movzbq %src8,%dst -> REX.W 0F B6 /r (zero-extend byte to 64) 595func x86_movzbq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 596 var p: i64 = o 597 out[p] = x86_rex_w(dst, src); p = p + 1 598 out[p] = 0x0F; p = p + 1 599 out[p] = 0xB6; p = p + 1 600 out[p] = x86_modrm(3, dst, src); p = p + 1 601 return p 602} 603// movzbq disp(%base),%dst (MEMORY byte load, zero-extend) -> REX.W 0F B6 /r (twin of x86_mov_load) 604func x86_movzbq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 605 var p: i64 = o 606 out[p] = x86_rex_w(dst, base); p = p + 1 607 out[p] = 0x0F; p = p + 1 608 out[p] = 0xB6; p = p + 1 609 return x86_mem_operand(out, p, dst, base, disp) 610} 611 612// ---- SIGN-extending subword loads (2026-07-10, subword sign-extend debt fix) -------------------- 613// The missing signed twins of movzbq/movzwq -- their ABSENCE is why the compiler backend went 614// zero-extend-only on every subword load (x86_emit_load_*_signed emitted mnemonics nxasm could not 615// assemble). Byte/word are the 0F BE / 0F BF opcode-siblings of movzbq's 0F B6; dword is MOVSXD 616// (REX.W 63 /r, no 0F prefix -- x86_mov_load's shape with a different opcode). movzwq (zero-word) 617// was ALSO missing and is added as the natural twin. 618// movsbq %src,%dst (reg-reg, sign-extend byte -> 64) -> REX.W 0F BE /r mod=3 619func x86_movsbq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 620 var p: i64 = o 621 out[p] = x86_rex_w(dst, src); p = p + 1 622 out[p] = 0x0F; p = p + 1 623 out[p] = 0xBE; p = p + 1 624 out[p] = x86_modrm(3, dst, src); p = p + 1 625 return p 626} 627// movsbq disp(%base),%dst (MEMORY byte load, SIGN-extend) -> REX.W 0F BE /r 628func x86_movsbq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 629 var p: i64 = o 630 out[p] = x86_rex_w(dst, base); p = p + 1 631 out[p] = 0x0F; p = p + 1 632 out[p] = 0xBE; p = p + 1 633 return x86_mem_operand(out, p, dst, base, disp) 634} 635// movswq %src,%dst (reg-reg, sign-extend word -> 64) -> REX.W 0F BF /r mod=3 636func x86_movswq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 637 var p: i64 = o 638 out[p] = x86_rex_w(dst, src); p = p + 1 639 out[p] = 0x0F; p = p + 1 640 out[p] = 0xBF; p = p + 1 641 out[p] = x86_modrm(3, dst, src); p = p + 1 642 return p 643} 644// movswq disp(%base),%dst (MEMORY word load, SIGN-extend) -> REX.W 0F BF /r 645func x86_movswq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 646 var p: i64 = o 647 out[p] = x86_rex_w(dst, base); p = p + 1 648 out[p] = 0x0F; p = p + 1 649 out[p] = 0xBF; p = p + 1 650 return x86_mem_operand(out, p, dst, base, disp) 651} 652// movzwq %src,%dst (reg-reg, ZERO-extend word -> 64) -> REX.W 0F B7 /r mod=3 653func x86_movzwq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 654 var p: i64 = o 655 out[p] = x86_rex_w(dst, src); p = p + 1 656 out[p] = 0x0F; p = p + 1 657 out[p] = 0xB7; p = p + 1 658 out[p] = x86_modrm(3, dst, src); p = p + 1 659 return p 660} 661// movzwq disp(%base),%dst (MEMORY word load, ZERO-extend) -> REX.W 0F B7 /r 662func x86_movzwq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 663 var p: i64 = o 664 out[p] = x86_rex_w(dst, base); p = p + 1 665 out[p] = 0x0F; p = p + 1 666 out[p] = 0xB7; p = p + 1 667 return x86_mem_operand(out, p, dst, base, disp) 668} 669// movslq %src,%dst (reg-reg MOVSXD, sign-extend dword -> 64) -> REX.W 63 /r mod=3 670func x86_movslq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 671 var p: i64 = o 672 out[p] = x86_rex_w(dst, src); p = p + 1 673 out[p] = 0x63; p = p + 1 674 out[p] = x86_modrm(3, dst, src); p = p + 1 675 return p 676} 677// movslq disp(%base),%dst (MEMORY dword load, SIGN-extend / MOVSXD) -> REX.W 63 /r 678func x86_movslq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 679 var p: i64 = o 680 out[p] = x86_rex_w(dst, base); p = p + 1 681 out[p] = 0x63; p = p + 1 682 return x86_mem_operand(out, p, dst, base, disp) 683} 684 685// cqo (sign-extend rax into rdx:rax for idiv) -> REX.W 99 686func x86_cqo(out: *u8, o: i64) -> i64 { 687 out[o] = 0x48; out[o + 1] = 0x99 688 return o + 2 689} 690 691// idivq %reg -> REX.W F7 /7 692func x86_idiv(out: *u8, o: i64, reg: i64) -> i64 { 693 var p: i64 = o 694 out[p] = x86_rex_w(0, reg); p = p + 1 695 out[p] = 0xF7; p = p + 1 696 out[p] = x86_modrm(3, 7, reg); p = p + 1 697 return p 698} 699 700// mulq %reg -> REX.W F7 /4 (UNSIGNED 64x64 -> rdx:rax = rax * reg). 701// Same F7 group as idiv (/7), neg (/3), not (/2); only the /digit differs. 702// REX.B (via x86_rex_w) extends reg to r8-r15. This is the __umulhi64 703// primitive (the rdx half) -- the wide-multiply core of every bignum/EC field-mul. 704func x86_mul(out: *u8, o: i64, reg: i64) -> i64 { 705 var p: i64 = o 706 out[p] = x86_rex_w(0, reg); p = p + 1 707 out[p] = 0xF7; p = p + 1 708 out[p] = x86_modrm(3, 4, reg); p = p + 1 709 return p 710} 711 712// crc32q %src,%dst -> F2 REX.W 0F 38 F1 /r (SSE4.2 CRC-32C/Castagnoli, 713// accumulate: dst = CRC32C(dst, src)). AT&T src,dst -> ModRM reg=dst (the 714// running CRC accumulator), rm=src (the 64-bit data word folded in). The 715// mandatory F2 prefix precedes REX.W (Intel SDM Vol.2 prefix order); REX.R 716// extends dst to r8-15, REX.B extends src. This is the hardware CRC-32C 717// primitive -- checksums, network-packet validation, hash-table fingerprints. 718func x86_crc32_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 719 var p: i64 = o 720 out[p] = 0xF2; p = p + 1 721 out[p] = x86_rex_w(dst, src); p = p + 1 722 out[p] = 0x0F; p = p + 1 723 out[p] = 0x38; p = p + 1 724 out[p] = 0xF1; p = p + 1 725 out[p] = x86_modrm(3, dst, src); p = p + 1 726 return p 727} 728 729// pdep %src2,%src1,%dst -> VEX.LZ.F2.0F38.W1 F5 /r (BMI2 parallel bit 730// DEPOSIT: scatter the low bits of src1 into the set-bit positions of the 731// mask src2, result in dst). AT&T src2,src1,dst -> dst=ModRM.reg, 732// src1=VEX.vvvv, src2=ModRM.rm. 3-byte VEX (0F38 map). Composes the proven 733// x86_vex3_rr encoder (mmmmm=2 for 0F38, W=1, pp=3 for F2, L=0). Deposit is 734// the varint-encode / bitboard-scatter / bit-interleave primitive. 735func x86_pdep_r64(out: *u8, o: i64, dst: i64, src1: i64, src2: i64) -> i64 { 736 return x86_vex3_rr(out, o, 2, 1, 0xF5, dst, src1, src2, 0, 3) 737} 738 739// pext %src2,%src1,%dst -> VEX.LZ.F3.0F38.W1 F5 /r (BMI2 parallel bit 740// EXTRACT: gather the src1 bits at the set-bit positions of the mask src2 741// down to the low bits of dst -- the inverse of pdep). Same operand layout 742// as pdep; only the mandatory prefix differs (pp=2 for F3). Extract is the 743// varint-decode / bitboard-gather / unicode-transcode / compression primitive. 744func x86_pext_r64(out: *u8, o: i64, dst: i64, src1: i64, src2: i64) -> i64 { 745 return x86_vex3_rr(out, o, 2, 1, 0xF5, dst, src1, src2, 0, 2) 746} 747 748// ---- BATCH 2: ADX/BMI2 wide-multiply dual-carry-chain (the P-256/bignum unlock) ---- 749// mulx dst_hi, dst_lo, src -> VEX.NDD.LZ.F2.0F38.W1 F6 /r (BMI2 flags-free 750// unsigned 64x64->128 multiply: src * implicit RDX; low half -> dst_lo, high half 751// -> dst_hi; leaves CF/OF UNTOUCHED so it interleaves with two add-carry chains). 752// Intel SDM operand map (VEX.NDD): dst_hi = VEX.vvvv, dst_lo = ModRM.reg, src = ModRM.rm. 753// Composing x86_vex3_rr(mmmmm=2 [0F38], W=1, opc=0xF6, pp=3 [F2], L=0), whose params are 754// (dst->ModRM.reg, src1->VEX.vvvv, src2->ModRM.rm) -> pass dst_lo as dst, dst_hi as src1, 755// src as src2. This is THE instruction the shelved u256_mul_wide_4x64 waited for. 756func x86_mulx_r64(out: *u8, o: i64, dst_hi: i64, dst_lo: i64, src: i64) -> i64 { 757 // Intel SDM operand encoding: ModRM.reg = r64a = dst_HI, VEX.vvvv = r64b = dst_LO, ModRM.rm = src. 758 // x86_vex3_rr maps its (dst -> ModRM.reg, src1 -> VEX.vvvv), so pass dst_hi as dst, dst_lo as src1. 759 return x86_vex3_rr(out, o, 2, 1, 0xF6, dst_hi, dst_lo, src, 0, 3) 760} 761 762// adcx dst,src -> 66 REX.W 0F 38 F6 /r (ADX: dst = dst + src + CF, updates ONLY CF). 763// adox dst,src -> F3 REX.W 0F 38 F6 /r (ADX: dst = dst + src + OF, updates ONLY OF). 764// The two run INDEPENDENT carry chains (CF vs OF) so a schoolbook column can accumulate 765// both carries in parallel around flags-free MULX -> ~1 cyc/limb (OpenSSL nistp256 path). 766// Same legacy-prefix 0F38 shape as x86_crc32_r64; only the mandatory prefix + opcode differ. 767// AT&T dst,src -> ModRM.reg = dst (accumulator), ModRM.rm = src. 768func x86_adcx_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 769 var p: i64 = o 770 out[p] = 0x66; p = p + 1 771 out[p] = x86_rex_w(dst, src); p = p + 1 772 out[p] = 0x0F; p = p + 1 773 out[p] = 0x38; p = p + 1 774 out[p] = 0xF6; p = p + 1 775 out[p] = x86_modrm(3, dst, src); p = p + 1 776 return p 777} 778func x86_adox_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 779 var p: i64 = o 780 out[p] = 0xF3; p = p + 1 781 out[p] = x86_rex_w(dst, src); p = p + 1 782 out[p] = 0x0F; p = p + 1 783 out[p] = 0x38; p = p + 1 784 out[p] = 0xF6; p = p + 1 785 out[p] = x86_modrm(3, dst, src); p = p + 1 786 return p 787} 788 789// shift $imm8,%reg -> REX.W C1 /ext ib (ext = SHL/SHR/SAR) 790func x86_shift_imm(out: *u8, o: i64, ext: i64, reg: i64, imm8: i64) -> i64 { 791 var p: i64 = o 792 out[p] = x86_rex_w(0, reg); p = p + 1 793 out[p] = 0xC1; p = p + 1 794 out[p] = x86_modrm(3, ext, reg); p = p + 1 795 out[p] = imm8 & 0xff; p = p + 1 796 return p 797} 798 799// jmp rel32 -> E9 cd (deterministic always-rel32, per charter) 800func x86_jmp_rel32(out: *u8, o: i64, rel: i64) -> i64 { 801 out[o] = 0xE9 802 return x86_put_u32le(out, o + 1, rel) 803} 804 805// call rel32 -> E8 cd 806func x86_call_rel32(out: *u8, o: i64, rel: i64) -> i64 { 807 out[o] = 0xE8 808 return x86_put_u32le(out, o + 1, rel) 809} 810 811// jcc rel32 -> 0F 80+cc cd 812func x86_jcc_rel32(out: *u8, o: i64, cc: i64, rel: i64) -> i64 { 813 out[o] = 0x0F 814 out[o + 1] = 0x80 + cc 815 return x86_put_u32le(out, o + 2, rel) 816} 817 818// cmovcc %src,%dst -> REX.W 0F 40+cc /r (reg=dst, rm=src). Conditional move, 819// flag-READ-only (never writes flags) -- the G22 bias-via-cmov division form. 820// Same two-byte reg,reg shape as x86_imul_rr. 821func x86_cmovcc(out: *u8, o: i64, cc: i64, dst: i64, src: i64) -> i64 { 822 var p: i64 = o 823 out[p] = x86_rex_w(dst, src); p = p + 1 824 out[p] = 0x0F; p = p + 1 825 out[p] = 0x40 + cc; p = p + 1 826 out[p] = x86_modrm(3, dst, src); p = p + 1 827 return p 828} 829 830// setcc %reg8 -> [REX] 0F 90+cc /0 (REX needed for spl/bpl/sil/dil + r8b-r15b) 831func x86_setcc(out: *u8, o: i64, cc: i64, reg: i64) -> i64 { 832 var p: i64 = o 833 if reg >= 4 { out[p] = 0x40 | ((reg >> 3) & 1); p = p + 1 } 834 out[p] = 0x0F; p = p + 1 835 out[p] = 0x90 + cc; p = p + 1 836 out[p] = x86_modrm(3, 0, reg); p = p + 1 837 return p 838} 839 840// ---- M2 opcode tail: rotates / bit-scan / unary / byte / indirect ---- 841// rotate $imm8,%reg -> REX.W C1 /(0=rol | 1=ror) ib (the __rotr64 class) 842func x86_rot_imm(out: *u8, o: i64, is_ror: i64, reg: i64, imm8: i64) -> i64 { 843 var p: i64 = o 844 out[p] = x86_rex_w(0, reg); p = p + 1 845 out[p] = 0xC1; p = p + 1 846 out[p] = x86_modrm(3, is_ror, reg); p = p + 1 847 out[p] = imm8 & 0xff; p = p + 1 848 return p 849} 850// rotate %cl,%reg -> REX.W D3 /(0|1) 851func x86_rot_cl(out: *u8, o: i64, is_ror: i64, reg: i64) -> i64 { 852 var p: i64 = o 853 out[p] = x86_rex_w(0, reg); p = p + 1 854 out[p] = 0xD3; p = p + 1 855 out[p] = x86_modrm(3, is_ror, reg); p = p + 1 856 return p 857} 858// shift %cl,%reg -> REX.W D3 /ext (ext = SHL=4 | SHR=5 | SAR=7) -- twin of x86_rot_cl 859func x86_shift_cl(out: *u8, o: i64, ext: i64, reg: i64) -> i64 { 860 var p: i64 = o 861 out[p] = x86_rex_w(0, reg); p = p + 1 862 out[p] = 0xD3; p = p + 1 863 out[p] = x86_modrm(3, ext, reg); p = p + 1 864 return p 865} 866// bswapq %reg -> REX.W 0F C8+rd 867func x86_bswap(out: *u8, o: i64, reg: i64) -> i64 { 868 var p: i64 = o 869 out[p] = 0x48 | ((reg >> 3) & 1); p = p + 1 870 out[p] = 0x0F; p = p + 1 871 out[p] = 0xC8 + (reg & 7); p = p + 1 872 return p 873} 874// popcntq %src,%dst -> F3 REX.W 0F B8 /r (F3 prefix precedes REX) 875func x86_popcnt(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 876 var p: i64 = o 877 out[p] = 0xF3; p = p + 1 878 out[p] = x86_rex_w(dst, src); p = p + 1 879 out[p] = 0x0F; p = p + 1 880 out[p] = 0xB8; p = p + 1 881 out[p] = x86_modrm(3, dst, src); p = p + 1 882 return p 883} 884// lzcntl/tzcntl %src,%dst (32-bit) -> F3 [REX] 0F BD|BC /r 885// REX (no W) only when an extended register (r8d+) is involved -- the 886// compiler's clz32/ctz32 lowering emits the %eax,%eax form. 887func x86_cnt32(out: *u8, o: i64, opc: i64, dst: i64, src: i64) -> i64 { 888 var p: i64 = o 889 out[p] = 0xF3; p = p + 1 890 if ((dst | src) & 8) != 0 { 891 var rex: i64 = 0x40 892 if (dst & 8) != 0 { rex = rex | 0x04 } // REX.R 893 if (src & 8) != 0 { rex = rex | 0x01 } // REX.B 894 out[p] = rex; p = p + 1 895 } 896 out[p] = 0x0F; p = p + 1 897 out[p] = opc; p = p + 1 898 out[p] = x86_modrm(3, dst, src); p = p + 1 899 return p 900} 901func x86_lzcnt32(out: *u8, o: i64, dst: i64, src: i64) -> i64 { return x86_cnt32(out, o, 0xBD, dst, src) } 902func x86_tzcnt32(out: *u8, o: i64, dst: i64, src: i64) -> i64 { return x86_cnt32(out, o, 0xBC, dst, src) } 903// REX.W F7 /ext unary: neg=/3, not=/2 904func x86_unary_f7(out: *u8, o: i64, ext: i64, reg: i64) -> i64 { 905 var p: i64 = o 906 out[p] = x86_rex_w(0, reg); p = p + 1 907 out[p] = 0xF7; p = p + 1 908 out[p] = x86_modrm(3, ext, reg); p = p + 1 909 return p 910} 911func x86_neg(out: *u8, o: i64, reg: i64) -> i64 { return x86_unary_f7(out, o, 3, reg) } 912func x86_not(out: *u8, o: i64, reg: i64) -> i64 { return x86_unary_f7(out, o, 2, reg) } 913// REX.W FF /ext: inc=/0, dec=/1 914func x86_incdec(out: *u8, o: i64, ext: i64, reg: i64) -> i64 { 915 var p: i64 = o 916 out[p] = x86_rex_w(0, reg); p = p + 1 917 out[p] = 0xFF; p = p + 1 918 out[p] = x86_modrm(3, ext, reg); p = p + 1 919 return p 920} 921// movb $imm8,disp(%base) -> C6 /0 + mem + ib (byte op: no REX.W) 922func x86_movb_imm(out: *u8, o: i64, base: i64, disp: i64, imm8: i64) -> i64 { 923 var p: i64 = o 924 if (base & 8) != 0 { out[p] = 0x41; p = p + 1 } // REX.B for r8-15 base 925 out[p] = 0xC6; p = p + 1 926 p = x86_mem_operand(out, p, 0, base, disp) 927 out[p] = imm8 & 0xff; p = p + 1 928 return p 929} 930// movb $imm8, %reg8 -> [REX] B0+rd ib (feat row asm-movb-imm-reg, 2026-06-10: 931// previously this form fell into the imm->MEM branch reading garbage BASE/DISP = 932// silent wrong bytes -> segfaulting ELF. Template-analogy from movb_store's REX rule.) 933func x86_movb_imm_reg(out: *u8, o: i64, reg: i64, imm8: i64) -> i64 { 934 var p: i64 = o 935 var rex: i64 = 0 936 if reg >= 4 { rex = 0x40 } // spl/bpl/sil/dil need REX 937 if (reg & 8) != 0 { rex = rex | 0x41 } // REX.B for r8b-r15b 938 if rex != 0 { out[p] = rex; p = p + 1 } 939 out[p] = 0xB0 + (reg & 7); p = p + 1 940 out[p] = imm8 & 0xff; p = p + 1 941 return p 942} 943// movb %src8,disp(%base) -> [REX] 88 /r + mem 944func x86_movb_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 { 945 var p: i64 = o 946 var rex: i64 = 0 947 if src >= 4 { rex = 0x40 } // spl/bpl/sil/dil need REX 948 if (src & 8) != 0 { rex = rex | 0x04 } // REX.R 949 if (base & 8) != 0 { rex = rex | 0x40 | 0x01 } // REX.B 950 if rex != 0 { out[p] = rex; p = p + 1 } 951 out[p] = 0x88; p = p + 1 952 return x86_mem_operand(out, p, src, base, disp) 953} 954// call *%reg -> FF /2 ; jmp *%reg -> FF /4 (indirect, default 64-bit) 955func x86_call_indirect(out: *u8, o: i64, reg: i64) -> i64 { 956 var p: i64 = o 957 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 } 958 out[p] = 0xFF; p = p + 1 959 out[p] = x86_modrm(3, 2, reg); p = p + 1 960 return p 961} 962func x86_jmp_indirect(out: *u8, o: i64, reg: i64) -> i64 { 963 var p: i64 = o 964 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 } 965 out[p] = 0xFF; p = p + 1 966 out[p] = x86_modrm(3, 4, reg); p = p + 1 967 return p 968} 969 970// ---- minimal static x86_64 ELF (ET_EXEC, one R+X PT_LOAD) ---- 971// 972// Layout: [0..64) ELF header, [64..120) program header, [120..) code. 973// One segment maps file offset 0 at vaddr X86_BASE (p_offset and 974// p_vaddr congruent mod p_align), entry = X86_BASE + X86_HDRLEN. 975// Returns total bytes written to `out` (X86_HDRLEN + code_len). 976func x86_build_elf_at(code: *u8, code_len: i64, entry_off: i64, out: *u8) -> i64 { 977 // e_ident 978 out[0] = 0x7F; out[1] = 0x45; out[2] = 0x4C; out[3] = 0x46 // \x7fELF 979 out[4] = 2 // EI_CLASS = ELFCLASS64 980 out[5] = 1 // EI_DATA = ELFDATA2LSB 981 out[6] = 1 // EI_VERSION 982 var i: i64 = 7 983 while i < 16 { out[i] = 0; i = i + 1 } // pad e_ident 984 x86_put_u16le(out, 16, 2) // e_type = ET_EXEC 985 x86_put_u16le(out, 18, EM_X86_64) // e_machine = EM_X86_64 986 x86_put_u32le(out, 20, 1) // e_version 987 let total: i64 = X86_HDRLEN + code_len 988 let entry: i64 = X86_BASE + X86_HDRLEN + entry_off 989 x86_put_u64le(out, 24, entry) // e_entry 990 x86_put_u64le(out, 32, 64) // e_phoff 991 x86_put_u64le(out, 40, 0) // e_shoff 992 x86_put_u32le(out, 48, 0) // e_flags 993 x86_put_u16le(out, 52, 64) // e_ehsize 994 x86_put_u16le(out, 54, 56) // e_phentsize 995 x86_put_u16le(out, 56, 1) // e_phnum 996 x86_put_u16le(out, 58, 0) // e_shentsize 997 x86_put_u16le(out, 60, 0) // e_shnum 998 x86_put_u16le(out, 62, 0) // e_shstrndx 999 // program header (PT_LOAD, R+X) 1000 x86_put_u32le(out, 64, 1) // p_type = PT_LOAD 1001 // RWX: .lcomm mutable globals live in the same single segment as code. 1002 // W^X DEBT (named): split a second RW PT_LOAD for the data tail. 1003 x86_put_u32le(out, 68, 7) // p_flags = PF_R | PF_W | PF_X 1004 x86_put_u64le(out, 72, 0) // p_offset 1005 x86_put_u64le(out, 80, X86_BASE) // p_vaddr 1006 x86_put_u64le(out, 88, X86_BASE) // p_paddr 1007 x86_put_u64le(out, 96, total) // p_filesz 1008 x86_put_u64le(out, 104, total) // p_memsz 1009 x86_put_u64le(out, 112, 0x1000) // p_align 1010 // code 1011 var k: i64 = 0 1012 while k < code_len { 1013 out[X86_HDRLEN + k] = code[k] 1014 k = k + 1 1015 } 1016 return total 1017} 1018 1019// Convenience wrapper: entry at the first code byte (entry_off = 0). 1020func x86_build_elf(code: *u8, code_len: i64, out: *u8) -> i64 { 1021 return x86_build_elf_at(code, code_len, 0, out) 1022}