code wiki / (root) / nxasm_x86_enc.nx

nxasm_x86_enc.nx source

↩ module page · 1173 lines · 49438 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) 406// LN18 (2026-09-01): the overflow pair -- OF=1 / OF=0 -- for the checked-arithmetic fuse (jo / jno). 407const X86_CC_O: i64 = 0 408const X86_CC_NO: i64 = 1 409const X86_CC_E: i64 = 4 410const X86_CC_NE: i64 = 5 411const X86_CC_L: i64 = 0xC 412const X86_CC_LE: i64 = 0xE 413const X86_CC_G: i64 = 0xF 414const X86_CC_GE: i64 = 0xD 415const X86_CC_S: i64 = 8 // SF=1 (negative) -- the G22 bias-select 416const X86_CC_NS: i64 = 9 // SF=0 417 418// ALU reg,reg: `<op>q %src,%dst` -> REX.W <opcode> ModRM(11 src dst) 419func x86_alu_rr(out: *u8, o: i64, opcode: i64, dst: i64, src: i64) -> i64 { 420 var p: i64 = o 421 out[p] = x86_rex_w(src, dst); p = p + 1 422 out[p] = opcode; p = p + 1 423 out[p] = x86_modrm(3, src, dst); p = p + 1 424 return p 425} 426 427// ALU imm,reg: `<op>q $imm,%dst`. imm8 form (83 /ext ib) when it fits a 428// signed byte, else imm32 form (81 /ext id) sign-extended to 64. 429func x86_alu_imm(out: *u8, o: i64, ext: i64, dst: i64, imm: i64) -> i64 { 430 var p: i64 = o 431 out[p] = x86_rex_w(0, dst); p = p + 1 432 var short: i64 = 0 433 if imm >= -128 { if imm <= 127 { short = 1 } } 434 if short == 1 { 435 out[p] = 0x83; p = p + 1 436 out[p] = x86_modrm(3, ext, dst); p = p + 1 437 out[p] = imm & 0xff; p = p + 1 438 return p 439 } 440 out[p] = 0x81; p = p + 1 441 out[p] = x86_modrm(3, ext, dst); p = p + 1 442 return x86_put_u32le(out, p, imm) 443} 444 445// Emit ModRM(+SIB+disp) for a `disp(%base)` memory operand with the 446// other operand in reg_field. mod=01(disp8) if disp fits a signed byte 447// else mod=10(disp32); base&7==4 (rsp/r12) requires the SIB byte 0x24. 448// (base==rbp with disp=0 still uses disp8=0, matching gas.) 449func x86_mem_operand(out: *u8, o: i64, reg_field: i64, base: i64, disp: i64) -> i64 { 450 var p: i64 = o 451 var mod: i64 = 2 452 if disp >= -128 { if disp <= 127 { mod = 1 } } 453 out[p] = x86_modrm(mod, reg_field, base & 7); p = p + 1 454 if (base & 7) == 4 { out[p] = 0x24; p = p + 1 } // SIB: scale0 index=none base=rsp 455 if mod == 1 { out[p] = disp & 0xff; return p + 1 } 456 return x86_put_u32le(out, p, disp) 457} 458 459// movq disp(%base),%dst (load) -> REX.W 8B /r 460func x86_mov_load(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 461 var p: i64 = o 462 out[p] = x86_rex_w(dst, base); p = p + 1 463 out[p] = 0x8B; p = p + 1 464 return x86_mem_operand(out, p, dst, base, disp) 465} 466 467// movq %src,disp(%base) (store) -> REX.W 89 /r 468func x86_mov_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 { 469 var p: i64 = o 470 out[p] = x86_rex_w(src, base); p = p + 1 471 out[p] = 0x89; p = p + 1 472 return x86_mem_operand(out, p, src, base, disp) 473} 474 475// ---- SIB (%base,%index,scale) addressing (2026-07-15, for array indexing) ---- 476// REX.W + X(index) + R(reg) + B(base). The plain x86_rex_w has no X bit, so 477// indexed addressing needs this variant. 478func x86_rex_wx(reg_field: i64, index_field: i64, base_field: i64) -> i64 { 479 var rex: i64 = 0x48 480 if (reg_field & 8) != 0 { rex = rex | 0x04 } // REX.R 481 if (index_field & 8) != 0 { rex = rex | 0x02 } // REX.X 482 if (base_field & 8) != 0 { rex = rex | 0x01 } // REX.B 483 return rex 484} 485 486// scale VALUE {1,2,4,8} -> the 2-bit SIB scale field {0,1,2,3}. 487func x86_scale_log2(scale: i64) -> i64 { 488 if scale == 8 { return 3 } 489 if scale == 4 { return 2 } 490 if scale == 2 { return 1 } 491 return 0 492} 493 494// ModRM(rm=100 => SIB) + SIB(scale,index,base) + disp for (%base,%index,scale). 495// mod=00 (no disp) when disp==0 AND base&7 != 5 (rbp/r13 need disp8=0); else 496// disp8/disp32. index MUST NOT be rsp (4) -- the SIB index=100 means "none"; 497// the compiler never selects rsp as an index, and the parser rejects it below. 498func x86_mem_operand_sib(out: *u8, o: i64, reg_field: i64, base: i64, 499 index: i64, scale: i64, disp: i64) -> i64 { 500 var p: i64 = o 501 var mod: i64 = 2 502 if disp >= -128 { if disp <= 127 { mod = 1 } } 503 if disp == 0 { if (base & 7) != 5 { mod = 0 } } 504 out[p] = x86_modrm(mod, reg_field, 4); p = p + 1 // rm=100 => SIB follows 505 out[p] = ((x86_scale_log2(scale) & 3) << 6) | ((index & 7) << 3) | (base & 7); p = p + 1 506 if mod == 0 { return p } 507 if mod == 1 { out[p] = disp & 0xff; return p + 1 } 508 return x86_put_u32le(out, p, disp) 509} 510 511// movq disp(%base,%index,scale),%dst (load) -> REX.WX 8B /r SIB 512func x86_mov_load_sib(out: *u8, o: i64, dst: i64, base: i64, index: i64, scale: i64, disp: i64) -> i64 { 513 var p: i64 = o 514 out[p] = x86_rex_wx(dst, index, base); p = p + 1 515 out[p] = 0x8B; p = p + 1 516 return x86_mem_operand_sib(out, p, dst, base, index, scale, disp) 517} 518 519// movq %src,disp(%base,%index,scale) (store) -> REX.WX 89 /r SIB 520func x86_mov_store_sib(out: *u8, o: i64, base: i64, index: i64, scale: i64, disp: i64, src: i64) -> i64 { 521 var p: i64 = o 522 out[p] = x86_rex_wx(src, index, base); p = p + 1 523 out[p] = 0x89; p = p + 1 524 return x86_mem_operand_sib(out, p, src, base, index, scale, disp) 525} 526 527// movl (32-bit): store 89 /r, load 8B /r -- NO REX.W; REX only for r8d+. 528// Emitted by the compiler for i32 struct-field stores (e.g. pollfd.fd). 529func x86_movl_rex_if(out: *u8, o: i64, reg: i64, rm: i64) -> i64 { 530 if ((reg | rm) & 8) == 0 { return o } 531 var rex: i64 = 0x40 532 if (reg & 8) != 0 { rex = rex | 0x04 } 533 if (rm & 8) != 0 { rex = rex | 0x01 } 534 out[o] = rex 535 return o + 1 536} 537func x86_movl_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 { 538 var p: i64 = x86_movl_rex_if(out, o, src, base) 539 out[p] = 0x89; p = p + 1 540 return x86_mem_operand(out, p, src, base, disp) 541} 542func x86_movl_load(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 543 var p: i64 = x86_movl_rex_if(out, o, dst, base) 544 out[p] = 0x8B; p = p + 1 545 return x86_mem_operand(out, p, dst, base, disp) 546} 547// movl %src,%dst (reg-reg) -> [REX] 89 /r mod=3 548func x86_movl_rr(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 549 var p: i64 = x86_movl_rex_if(out, o, src, dst) 550 out[p] = 0x89; p = p + 1 551 out[p] = x86_modrm(3, src, dst); p = p + 1 552 return p 553} 554 555// leaq disp(%base),%dst -> REX.W 8D /r 556func x86_lea(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 557 var p: i64 = o 558 out[p] = x86_rex_w(dst, base); p = p + 1 559 out[p] = 0x8D; p = p + 1 560 return x86_mem_operand(out, p, dst, base, disp) 561} 562 563// leaq disp(%base,%index,scale),%dst -> REX.WRXB 8D /r SIB. 564// Same ModRM/SIB/disp shape as x86_mov_load_sib (8B), opcode 8D. The address 565// arithmetic (base + index*scale + disp) is COMPUTED into dst, no memory touch; 566// this is the lea-strength peephole target (c*n+d => leaq d(%n,%n,s) for 567// c in {2,3,5,9} with base==index==n). 568func x86_lea_sib(out: *u8, o: i64, dst: i64, base: i64, index: i64, scale: i64, disp: i64) -> i64 { 569 var p: i64 = o 570 out[p] = x86_rex_wx(dst, index, base); p = p + 1 571 out[p] = 0x8D; p = p + 1 572 return x86_mem_operand_sib(out, p, dst, base, index, scale, disp) 573} 574 575// leaq disp(%rip),%dst -> REX.W 8D /r, ModRM mod=00 reg=dst rm=101, disp32. 576// disp32 is relative to the END of this 7-byte instruction (RIP-relative). 577func x86_lea_rip(out: *u8, o: i64, dst: i64, disp32: i64) -> i64 { 578 var p: i64 = o 579 var rex: i64 = 0x48 580 if (dst & 8) != 0 { rex = rex | 0x04 } // REX.R 581 out[p] = rex; p = p + 1 582 out[p] = 0x8D; p = p + 1 583 out[p] = x86_modrm(0, dst, 5); p = p + 1 // mod=00 rm=101 => RIP-relative 584 return x86_put_u32le(out, p, disp32) 585} 586 587// imulq %src,%dst -> REX.W 0F AF /r (reg=dst, rm=src) 588func x86_imul_rr(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 589 var p: i64 = o 590 out[p] = x86_rex_w(dst, src); p = p + 1 591 out[p] = 0x0F; p = p + 1 592 out[p] = 0xAF; p = p + 1 593 out[p] = x86_modrm(3, dst, src); p = p + 1 594 return p 595} 596 597// movzbq %src8,%dst -> REX.W 0F B6 /r (zero-extend byte to 64) 598func x86_movzbq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 599 var p: i64 = o 600 out[p] = x86_rex_w(dst, src); p = p + 1 601 out[p] = 0x0F; p = p + 1 602 out[p] = 0xB6; p = p + 1 603 out[p] = x86_modrm(3, dst, src); p = p + 1 604 return p 605} 606// movzbq disp(%base),%dst (MEMORY byte load, zero-extend) -> REX.W 0F B6 /r (twin of x86_mov_load) 607func x86_movzbq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 608 var p: i64 = o 609 out[p] = x86_rex_w(dst, base); p = p + 1 610 out[p] = 0x0F; p = p + 1 611 out[p] = 0xB6; p = p + 1 612 return x86_mem_operand(out, p, dst, base, disp) 613} 614 615// ---- SIGN-extending subword loads (2026-07-10, subword sign-extend debt fix) -------------------- 616// The missing signed twins of movzbq/movzwq -- their ABSENCE is why the compiler backend went 617// zero-extend-only on every subword load (x86_emit_load_*_signed emitted mnemonics nxasm could not 618// assemble). Byte/word are the 0F BE / 0F BF opcode-siblings of movzbq's 0F B6; dword is MOVSXD 619// (REX.W 63 /r, no 0F prefix -- x86_mov_load's shape with a different opcode). movzwq (zero-word) 620// was ALSO missing and is added as the natural twin. 621// movsbq %src,%dst (reg-reg, sign-extend byte -> 64) -> REX.W 0F BE /r mod=3 622func x86_movsbq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 623 var p: i64 = o 624 out[p] = x86_rex_w(dst, src); p = p + 1 625 out[p] = 0x0F; p = p + 1 626 out[p] = 0xBE; p = p + 1 627 out[p] = x86_modrm(3, dst, src); p = p + 1 628 return p 629} 630// movsbq disp(%base),%dst (MEMORY byte load, SIGN-extend) -> REX.W 0F BE /r 631func x86_movsbq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 632 var p: i64 = o 633 out[p] = x86_rex_w(dst, base); p = p + 1 634 out[p] = 0x0F; p = p + 1 635 out[p] = 0xBE; p = p + 1 636 return x86_mem_operand(out, p, dst, base, disp) 637} 638// movswq %src,%dst (reg-reg, sign-extend word -> 64) -> REX.W 0F BF /r mod=3 639func x86_movswq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 640 var p: i64 = o 641 out[p] = x86_rex_w(dst, src); p = p + 1 642 out[p] = 0x0F; p = p + 1 643 out[p] = 0xBF; p = p + 1 644 out[p] = x86_modrm(3, dst, src); p = p + 1 645 return p 646} 647// movswq disp(%base),%dst (MEMORY word load, SIGN-extend) -> REX.W 0F BF /r 648func x86_movswq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 649 var p: i64 = o 650 out[p] = x86_rex_w(dst, base); p = p + 1 651 out[p] = 0x0F; p = p + 1 652 out[p] = 0xBF; p = p + 1 653 return x86_mem_operand(out, p, dst, base, disp) 654} 655// movzwq %src,%dst (reg-reg, ZERO-extend word -> 64) -> REX.W 0F B7 /r mod=3 656func x86_movzwq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 657 var p: i64 = o 658 out[p] = x86_rex_w(dst, src); p = p + 1 659 out[p] = 0x0F; p = p + 1 660 out[p] = 0xB7; p = p + 1 661 out[p] = x86_modrm(3, dst, src); p = p + 1 662 return p 663} 664// movzwq disp(%base),%dst (MEMORY word load, ZERO-extend) -> REX.W 0F B7 /r 665func x86_movzwq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 666 var p: i64 = o 667 out[p] = x86_rex_w(dst, base); p = p + 1 668 out[p] = 0x0F; p = p + 1 669 out[p] = 0xB7; p = p + 1 670 return x86_mem_operand(out, p, dst, base, disp) 671} 672// movslq %src,%dst (reg-reg MOVSXD, sign-extend dword -> 64) -> REX.W 63 /r mod=3 673func x86_movslq(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 674 var p: i64 = o 675 out[p] = x86_rex_w(dst, src); p = p + 1 676 out[p] = 0x63; p = p + 1 677 out[p] = x86_modrm(3, dst, src); p = p + 1 678 return p 679} 680// movslq disp(%base),%dst (MEMORY dword load, SIGN-extend / MOVSXD) -> REX.W 63 /r 681func x86_movslq_mem(out: *u8, o: i64, dst: i64, base: i64, disp: i64) -> i64 { 682 var p: i64 = o 683 out[p] = x86_rex_w(dst, base); p = p + 1 684 out[p] = 0x63; p = p + 1 685 return x86_mem_operand(out, p, dst, base, disp) 686} 687 688// cqo (sign-extend rax into rdx:rax for idiv) -> REX.W 99 689func x86_cqo(out: *u8, o: i64) -> i64 { 690 out[o] = 0x48; out[o + 1] = 0x99 691 return o + 2 692} 693 694// idivq %reg -> REX.W F7 /7 695func x86_idiv(out: *u8, o: i64, reg: i64) -> i64 { 696 var p: i64 = o 697 out[p] = x86_rex_w(0, reg); p = p + 1 698 out[p] = 0xF7; p = p + 1 699 out[p] = x86_modrm(3, 7, reg); p = p + 1 700 return p 701} 702 703// mulq %reg -> REX.W F7 /4 (UNSIGNED 64x64 -> rdx:rax = rax * reg). 704// Same F7 group as idiv (/7), neg (/3), not (/2); only the /digit differs. 705// REX.B (via x86_rex_w) extends reg to r8-r15. This is the __umulhi64 706// primitive (the rdx half) -- the wide-multiply core of every bignum/EC field-mul. 707func x86_mul(out: *u8, o: i64, reg: i64) -> i64 { 708 var p: i64 = o 709 out[p] = x86_rex_w(0, reg); p = p + 1 710 out[p] = 0xF7; p = p + 1 711 out[p] = x86_modrm(3, 4, reg); p = p + 1 712 return p 713} 714 715// crc32q %src,%dst -> F2 REX.W 0F 38 F1 /r (SSE4.2 CRC-32C/Castagnoli, 716// accumulate: dst = CRC32C(dst, src)). AT&T src,dst -> ModRM reg=dst (the 717// running CRC accumulator), rm=src (the 64-bit data word folded in). The 718// mandatory F2 prefix precedes REX.W (Intel SDM Vol.2 prefix order); REX.R 719// extends dst to r8-15, REX.B extends src. This is the hardware CRC-32C 720// primitive -- checksums, network-packet validation, hash-table fingerprints. 721func x86_crc32_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 722 var p: i64 = o 723 out[p] = 0xF2; p = p + 1 724 out[p] = x86_rex_w(dst, src); p = p + 1 725 out[p] = 0x0F; p = p + 1 726 out[p] = 0x38; p = p + 1 727 out[p] = 0xF1; p = p + 1 728 out[p] = x86_modrm(3, dst, src); p = p + 1 729 return p 730} 731 732// pdep %src2,%src1,%dst -> VEX.LZ.F2.0F38.W1 F5 /r (BMI2 parallel bit 733// DEPOSIT: scatter the low bits of src1 into the set-bit positions of the 734// mask src2, result in dst). AT&T src2,src1,dst -> dst=ModRM.reg, 735// src1=VEX.vvvv, src2=ModRM.rm. 3-byte VEX (0F38 map). Composes the proven 736// x86_vex3_rr encoder (mmmmm=2 for 0F38, W=1, pp=3 for F2, L=0). Deposit is 737// the varint-encode / bitboard-scatter / bit-interleave primitive. 738func x86_pdep_r64(out: *u8, o: i64, dst: i64, src1: i64, src2: i64) -> i64 { 739 return x86_vex3_rr(out, o, 2, 1, 0xF5, dst, src1, src2, 0, 3) 740} 741 742// pext %src2,%src1,%dst -> VEX.LZ.F3.0F38.W1 F5 /r (BMI2 parallel bit 743// EXTRACT: gather the src1 bits at the set-bit positions of the mask src2 744// down to the low bits of dst -- the inverse of pdep). Same operand layout 745// as pdep; only the mandatory prefix differs (pp=2 for F3). Extract is the 746// varint-decode / bitboard-gather / unicode-transcode / compression primitive. 747func x86_pext_r64(out: *u8, o: i64, dst: i64, src1: i64, src2: i64) -> i64 { 748 return x86_vex3_rr(out, o, 2, 1, 0xF5, dst, src1, src2, 0, 2) 749} 750 751// ---- BATCH 2: ADX/BMI2 wide-multiply dual-carry-chain (the P-256/bignum unlock) ---- 752// mulx dst_hi, dst_lo, src -> VEX.NDD.LZ.F2.0F38.W1 F6 /r (BMI2 flags-free 753// unsigned 64x64->128 multiply: src * implicit RDX; low half -> dst_lo, high half 754// -> dst_hi; leaves CF/OF UNTOUCHED so it interleaves with two add-carry chains). 755// Intel SDM operand map (VEX.NDD): dst_hi = VEX.vvvv, dst_lo = ModRM.reg, src = ModRM.rm. 756// Composing x86_vex3_rr(mmmmm=2 [0F38], W=1, opc=0xF6, pp=3 [F2], L=0), whose params are 757// (dst->ModRM.reg, src1->VEX.vvvv, src2->ModRM.rm) -> pass dst_lo as dst, dst_hi as src1, 758// src as src2. This is THE instruction the shelved u256_mul_wide_4x64 waited for. 759func x86_mulx_r64(out: *u8, o: i64, dst_hi: i64, dst_lo: i64, src: i64) -> i64 { 760 // Intel SDM operand encoding: ModRM.reg = r64a = dst_HI, VEX.vvvv = r64b = dst_LO, ModRM.rm = src. 761 // x86_vex3_rr maps its (dst -> ModRM.reg, src1 -> VEX.vvvv), so pass dst_hi as dst, dst_lo as src1. 762 return x86_vex3_rr(out, o, 2, 1, 0xF6, dst_hi, dst_lo, src, 0, 3) 763} 764 765// adcx dst,src -> 66 REX.W 0F 38 F6 /r (ADX: dst = dst + src + CF, updates ONLY CF). 766// adox dst,src -> F3 REX.W 0F 38 F6 /r (ADX: dst = dst + src + OF, updates ONLY OF). 767// The two run INDEPENDENT carry chains (CF vs OF) so a schoolbook column can accumulate 768// both carries in parallel around flags-free MULX -> ~1 cyc/limb (OpenSSL nistp256 path). 769// Same legacy-prefix 0F38 shape as x86_crc32_r64; only the mandatory prefix + opcode differ. 770// AT&T dst,src -> ModRM.reg = dst (accumulator), ModRM.rm = src. 771func x86_adcx_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 772 var p: i64 = o 773 out[p] = 0x66; p = p + 1 774 out[p] = x86_rex_w(dst, src); p = p + 1 775 out[p] = 0x0F; p = p + 1 776 out[p] = 0x38; p = p + 1 777 out[p] = 0xF6; p = p + 1 778 out[p] = x86_modrm(3, dst, src); p = p + 1 779 return p 780} 781func x86_adox_r64(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 782 var p: i64 = o 783 out[p] = 0xF3; p = p + 1 784 out[p] = x86_rex_w(dst, src); p = p + 1 785 out[p] = 0x0F; p = p + 1 786 out[p] = 0x38; p = p + 1 787 out[p] = 0xF6; p = p + 1 788 out[p] = x86_modrm(3, dst, src); p = p + 1 789 return p 790} 791 792// shift $imm8,%reg -> REX.W C1 /ext ib (ext = SHL/SHR/SAR) 793func x86_shift_imm(out: *u8, o: i64, ext: i64, reg: i64, imm8: i64) -> i64 { 794 var p: i64 = o 795 out[p] = x86_rex_w(0, reg); p = p + 1 796 out[p] = 0xC1; p = p + 1 797 out[p] = x86_modrm(3, ext, reg); p = p + 1 798 out[p] = imm8 & 0xff; p = p + 1 799 return p 800} 801 802// jmp rel32 -> E9 cd (deterministic always-rel32, per charter) 803func x86_jmp_rel32(out: *u8, o: i64, rel: i64) -> i64 { 804 out[o] = 0xE9 805 return x86_put_u32le(out, o + 1, rel) 806} 807 808// call rel32 -> E8 cd 809func x86_call_rel32(out: *u8, o: i64, rel: i64) -> i64 { 810 out[o] = 0xE8 811 return x86_put_u32le(out, o + 1, rel) 812} 813 814// jcc rel32 -> 0F 80+cc cd 815func x86_jcc_rel32(out: *u8, o: i64, cc: i64, rel: i64) -> i64 { 816 out[o] = 0x0F 817 out[o + 1] = 0x80 + cc 818 return x86_put_u32le(out, o + 2, rel) 819} 820 821// cmovcc %src,%dst -> REX.W 0F 40+cc /r (reg=dst, rm=src). Conditional move, 822// flag-READ-only (never writes flags) -- the G22 bias-via-cmov division form. 823// Same two-byte reg,reg shape as x86_imul_rr. 824func x86_cmovcc(out: *u8, o: i64, cc: i64, dst: i64, src: i64) -> i64 { 825 var p: i64 = o 826 out[p] = x86_rex_w(dst, src); p = p + 1 827 out[p] = 0x0F; p = p + 1 828 out[p] = 0x40 + cc; p = p + 1 829 out[p] = x86_modrm(3, dst, src); p = p + 1 830 return p 831} 832 833// setcc %reg8 -> [REX] 0F 90+cc /0 (REX needed for spl/bpl/sil/dil + r8b-r15b) 834func x86_setcc(out: *u8, o: i64, cc: i64, reg: i64) -> i64 { 835 var p: i64 = o 836 if reg >= 4 { out[p] = 0x40 | ((reg >> 3) & 1); p = p + 1 } 837 out[p] = 0x0F; p = p + 1 838 out[p] = 0x90 + cc; p = p + 1 839 out[p] = x86_modrm(3, 0, reg); p = p + 1 840 return p 841} 842 843// ---- M2 opcode tail: rotates / bit-scan / unary / byte / indirect ---- 844// rotate $imm8,%reg -> REX.W C1 /(0=rol | 1=ror) ib (the __rotr64 class) 845func x86_rot_imm(out: *u8, o: i64, is_ror: i64, reg: i64, imm8: i64) -> i64 { 846 var p: i64 = o 847 out[p] = x86_rex_w(0, reg); p = p + 1 848 out[p] = 0xC1; p = p + 1 849 out[p] = x86_modrm(3, is_ror, reg); p = p + 1 850 out[p] = imm8 & 0xff; p = p + 1 851 return p 852} 853// rotate %cl,%reg -> REX.W D3 /(0|1) 854func x86_rot_cl(out: *u8, o: i64, is_ror: i64, reg: i64) -> i64 { 855 var p: i64 = o 856 out[p] = x86_rex_w(0, reg); p = p + 1 857 out[p] = 0xD3; p = p + 1 858 out[p] = x86_modrm(3, is_ror, reg); p = p + 1 859 return p 860} 861// shift %cl,%reg -> REX.W D3 /ext (ext = SHL=4 | SHR=5 | SAR=7) -- twin of x86_rot_cl 862func x86_shift_cl(out: *u8, o: i64, ext: i64, reg: i64) -> i64 { 863 var p: i64 = o 864 out[p] = x86_rex_w(0, reg); p = p + 1 865 out[p] = 0xD3; p = p + 1 866 out[p] = x86_modrm(3, ext, reg); p = p + 1 867 return p 868} 869// bswapq %reg -> REX.W 0F C8+rd 870func x86_bswap(out: *u8, o: i64, reg: i64) -> i64 { 871 var p: i64 = o 872 out[p] = 0x48 | ((reg >> 3) & 1); p = p + 1 873 out[p] = 0x0F; p = p + 1 874 out[p] = 0xC8 + (reg & 7); p = p + 1 875 return p 876} 877// popcntq %src,%dst -> F3 REX.W 0F B8 /r (F3 prefix precedes REX) 878func x86_popcnt(out: *u8, o: i64, dst: i64, src: i64) -> i64 { 879 var p: i64 = o 880 out[p] = 0xF3; p = p + 1 881 out[p] = x86_rex_w(dst, src); p = p + 1 882 out[p] = 0x0F; p = p + 1 883 out[p] = 0xB8; p = p + 1 884 out[p] = x86_modrm(3, dst, src); p = p + 1 885 return p 886} 887// lzcntl/tzcntl %src,%dst (32-bit) -> F3 [REX] 0F BD|BC /r 888// REX (no W) only when an extended register (r8d+) is involved -- the 889// compiler's clz32/ctz32 lowering emits the %eax,%eax form. 890func x86_cnt32(out: *u8, o: i64, opc: i64, dst: i64, src: i64) -> i64 { 891 var p: i64 = o 892 out[p] = 0xF3; p = p + 1 893 if ((dst | src) & 8) != 0 { 894 var rex: i64 = 0x40 895 if (dst & 8) != 0 { rex = rex | 0x04 } // REX.R 896 if (src & 8) != 0 { rex = rex | 0x01 } // REX.B 897 out[p] = rex; p = p + 1 898 } 899 out[p] = 0x0F; p = p + 1 900 out[p] = opc; p = p + 1 901 out[p] = x86_modrm(3, dst, src); p = p + 1 902 return p 903} 904func x86_lzcnt32(out: *u8, o: i64, dst: i64, src: i64) -> i64 { return x86_cnt32(out, o, 0xBD, dst, src) } 905func x86_tzcnt32(out: *u8, o: i64, dst: i64, src: i64) -> i64 { return x86_cnt32(out, o, 0xBC, dst, src) } 906// REX.W F7 /ext unary: neg=/3, not=/2 907func x86_unary_f7(out: *u8, o: i64, ext: i64, reg: i64) -> i64 { 908 var p: i64 = o 909 out[p] = x86_rex_w(0, reg); p = p + 1 910 out[p] = 0xF7; p = p + 1 911 out[p] = x86_modrm(3, ext, reg); p = p + 1 912 return p 913} 914func x86_neg(out: *u8, o: i64, reg: i64) -> i64 { return x86_unary_f7(out, o, 3, reg) } 915func x86_not(out: *u8, o: i64, reg: i64) -> i64 { return x86_unary_f7(out, o, 2, reg) } 916// REX.W FF /ext: inc=/0, dec=/1 917func x86_incdec(out: *u8, o: i64, ext: i64, reg: i64) -> i64 { 918 var p: i64 = o 919 out[p] = x86_rex_w(0, reg); p = p + 1 920 out[p] = 0xFF; p = p + 1 921 out[p] = x86_modrm(3, ext, reg); p = p + 1 922 return p 923} 924// movb $imm8,disp(%base) -> C6 /0 + mem + ib (byte op: no REX.W) 925func x86_movb_imm(out: *u8, o: i64, base: i64, disp: i64, imm8: i64) -> i64 { 926 var p: i64 = o 927 if (base & 8) != 0 { out[p] = 0x41; p = p + 1 } // REX.B for r8-15 base 928 out[p] = 0xC6; p = p + 1 929 p = x86_mem_operand(out, p, 0, base, disp) 930 out[p] = imm8 & 0xff; p = p + 1 931 return p 932} 933// movb $imm8, %reg8 -> [REX] B0+rd ib (feat row asm-movb-imm-reg, 2026-06-10: 934// previously this form fell into the imm->MEM branch reading garbage BASE/DISP = 935// silent wrong bytes -> segfaulting ELF. Template-analogy from movb_store's REX rule.) 936func x86_movb_imm_reg(out: *u8, o: i64, reg: i64, imm8: i64) -> i64 { 937 var p: i64 = o 938 var rex: i64 = 0 939 if reg >= 4 { rex = 0x40 } // spl/bpl/sil/dil need REX 940 if (reg & 8) != 0 { rex = rex | 0x41 } // REX.B for r8b-r15b 941 if rex != 0 { out[p] = rex; p = p + 1 } 942 out[p] = 0xB0 + (reg & 7); p = p + 1 943 out[p] = imm8 & 0xff; p = p + 1 944 return p 945} 946// movb %src8,disp(%base) -> [REX] 88 /r + mem 947func x86_movb_store(out: *u8, o: i64, base: i64, disp: i64, src: i64) -> i64 { 948 var p: i64 = o 949 var rex: i64 = 0 950 if src >= 4 { rex = 0x40 } // spl/bpl/sil/dil need REX 951 if (src & 8) != 0 { rex = rex | 0x04 } // REX.R 952 if (base & 8) != 0 { rex = rex | 0x40 | 0x01 } // REX.B 953 if rex != 0 { out[p] = rex; p = p + 1 } 954 out[p] = 0x88; p = p + 1 955 return x86_mem_operand(out, p, src, base, disp) 956} 957// call *%reg -> FF /2 ; jmp *%reg -> FF /4 (indirect, default 64-bit) 958func x86_call_indirect(out: *u8, o: i64, reg: i64) -> i64 { 959 var p: i64 = o 960 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 } 961 out[p] = 0xFF; p = p + 1 962 out[p] = x86_modrm(3, 2, reg); p = p + 1 963 return p 964} 965func x86_jmp_indirect(out: *u8, o: i64, reg: i64) -> i64 { 966 var p: i64 = o 967 if (reg & 8) != 0 { out[p] = 0x41; p = p + 1 } 968 out[p] = 0xFF; p = p + 1 969 out[p] = x86_modrm(3, 4, reg); p = p + 1 970 return p 971} 972 973// ---- minimal static x86_64 ELF (ET_EXEC, one R+X PT_LOAD) ---- 974// 975// Layout: [0..64) ELF header, [64..120) program header, [120..) code. 976// One segment maps file offset 0 at vaddr X86_BASE (p_offset and 977// p_vaddr congruent mod p_align), entry = X86_BASE + X86_HDRLEN. 978// Returns total bytes written to `out` (X86_HDRLEN + code_len). 979func x86_build_elf_at(code: *u8, code_len: i64, entry_off: i64, out: *u8) -> i64 { 980 // e_ident 981 out[0] = 0x7F; out[1] = 0x45; out[2] = 0x4C; out[3] = 0x46 // \x7fELF 982 out[4] = 2 // EI_CLASS = ELFCLASS64 983 out[5] = 1 // EI_DATA = ELFDATA2LSB 984 out[6] = 1 // EI_VERSION 985 var i: i64 = 7 986 while i < 16 { out[i] = 0; i = i + 1 } // pad e_ident 987 x86_put_u16le(out, 16, 2) // e_type = ET_EXEC 988 x86_put_u16le(out, 18, EM_X86_64) // e_machine = EM_X86_64 989 x86_put_u32le(out, 20, 1) // e_version 990 let total: i64 = X86_HDRLEN + code_len 991 let entry: i64 = X86_BASE + X86_HDRLEN + entry_off 992 x86_put_u64le(out, 24, entry) // e_entry 993 x86_put_u64le(out, 32, 64) // e_phoff 994 x86_put_u64le(out, 40, 0) // e_shoff 995 x86_put_u32le(out, 48, 0) // e_flags 996 x86_put_u16le(out, 52, 64) // e_ehsize 997 x86_put_u16le(out, 54, 56) // e_phentsize 998 x86_put_u16le(out, 56, 1) // e_phnum 999 x86_put_u16le(out, 58, 0) // e_shentsize 1000 x86_put_u16le(out, 60, 0) // e_shnum 1001 x86_put_u16le(out, 62, 0) // e_shstrndx 1002 // program header (PT_LOAD, R+X) 1003 x86_put_u32le(out, 64, 1) // p_type = PT_LOAD 1004 // RWX: .lcomm mutable globals live in the same single segment as code. 1005 // W^X DEBT (named): split a second RW PT_LOAD for the data tail. 1006 x86_put_u32le(out, 68, 7) // p_flags = PF_R | PF_W | PF_X 1007 x86_put_u64le(out, 72, 0) // p_offset 1008 x86_put_u64le(out, 80, X86_BASE) // p_vaddr 1009 x86_put_u64le(out, 88, X86_BASE) // p_paddr 1010 x86_put_u64le(out, 96, total) // p_filesz 1011 x86_put_u64le(out, 104, total) // p_memsz 1012 x86_put_u64le(out, 112, 0x1000) // p_align 1013 // code 1014 var k: i64 = 0 1015 while k < code_len { 1016 out[X86_HDRLEN + k] = code[k] 1017 k = k + 1 1018 } 1019 return total 1020} 1021 1022// ---- DEBUG-AWARE VARIANT (DDR-002, 2026-08-07) ----------------------------- 1023// 1024// Same image as x86_build_elf_at, plus a SECTION HEADER TABLE and a .debug_line section. 1025// 1026// WHY THIS IS SAFE TO ADD TO THE CROWN JEWEL: the SHT and .debug_line are appended PAST the 1027// PT_LOAD (p_filesz/p_memsz still cover exactly X86_HDRLEN + code_len), so the loader never reads 1028// a byte of them. An ELF with a wrong section table still executes correctly; only tools look at 1029// sections. Measured, not hoped: the layout was read out of this function before it was extended. 1030// 1031// WHY dbg_len == 0 DELEGATES: a build without -g emits no .file/.loc, so the assembler has no rows, 1032// so this returns the ORIGINAL bytes -- byte-identical to a toolchain that never heard of DWARF. 1033// That is what keeps the published 168-byte minimal static binary intact (DDR-002 section 6.1), 1034// and it is checkable: the equivalence gate must show ea == eb on every row. 1035const X86_SHT_ENTS: i64 = 4 // NULL, .text, .debug_line, .shstrtab 1036const X86_SHT_ENTSZ: i64 = 64 1037 1038// Copy a NUL-terminated literal into `out` at `off`, including its terminator. 1039// Returns bytes written. Length computed at runtime -- a hand-counted section-name table is the 1040// same bug class that has twice silently truncated diagnostics in this tree. 1041func x86_sh_str(out: *u8, off: i64, s: *u8) -> i64 { 1042 var i: i64 = 0 1043 while s[i] != (0 as u8) { out[off + i] = s[i]; i = i + 1 } 1044 out[off + i] = 0 1045 return i + 1 1046} 1047 1048func x86_sh_ent(out: *u8, at: i64, name: i64, styp: i64, flags: i64, 1049 addr: i64, soff: i64, ssz: i64, align: i64) -> i64 { 1050 var z: i64 = 0 1051 while z < X86_SHT_ENTSZ { out[at + z] = 0; z = z + 1 } 1052 x86_put_u32le(out, at + 0, name) 1053 x86_put_u32le(out, at + 4, styp) 1054 x86_put_u64le(out, at + 8, flags) 1055 x86_put_u64le(out, at + 16, addr) 1056 x86_put_u64le(out, at + 24, soff) 1057 x86_put_u64le(out, at + 32, ssz) 1058 x86_put_u64le(out, at + 48, align) 1059 return 0 1060} 1061 1062func x86_build_elf_dbg_at(code: *u8, code_len: i64, entry_off: i64, out: *u8, 1063 dbg: *u8, dbg_len: i64) -> i64 { 1064 let total: i64 = x86_build_elf_at(code, code_len, entry_off, out) 1065 if dbg_len <= 0 { return total } 1066 1067 // .debug_line immediately after the loaded image. 1068 let dbg_off: i64 = total 1069 var i: i64 = 0 1070 while i < dbg_len { out[dbg_off + i] = dbg[i]; i = i + 1 } 1071 1072 // .shstrtab: leading NUL, then the three names. 1073 let str_off: i64 = dbg_off + dbg_len 1074 var so: i64 = str_off 1075 out[so] = 0 1076 so = so + 1 1077 let n_text: i64 = so - str_off 1078 so = so + x86_sh_str(out, so, ".text" as *u8) 1079 let n_dbg: i64 = so - str_off 1080 so = so + x86_sh_str(out, so, ".debug_line" as *u8) 1081 let n_str: i64 = so - str_off 1082 so = so + x86_sh_str(out, so, ".shstrtab" as *u8) 1083 let str_len: i64 = so - str_off 1084 1085 // Section headers are 8-aligned by convention; pad explicitly rather than assume. 1086 var sht_off: i64 = so 1087 while (sht_off & 7) != 0 { out[sht_off] = 0; sht_off = sht_off + 1 } 1088 1089 x86_sh_ent(out, sht_off, 0, 0, 0, 0, 0, 0, 0) 1090 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ, n_text, 1, 6, 1091 X86_BASE + X86_HDRLEN, X86_HDRLEN, code_len, 16) 1092 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*2, n_dbg, 1, 0, 0, dbg_off, dbg_len, 1) 1093 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*3, n_str, 3, 0, 0, str_off, str_len, 1) 1094 1095 x86_put_u64le(out, 40, sht_off) // e_shoff 1096 x86_put_u16le(out, 58, X86_SHT_ENTSZ) // e_shentsize 1097 x86_put_u16le(out, 60, X86_SHT_ENTS) // e_shnum 1098 x86_put_u16le(out, 62, 3) // e_shstrndx -> .shstrtab 1099 1100 return sht_off + X86_SHT_ENTSZ * X86_SHT_ENTS 1101} 1102 1103// DDR-009 step 2: the SAME image plus THREE debug sections (.debug_line, .debug_info, 1104// .debug_abbrev). A SEPARATE function, not an arity change to x86_build_elf_dbg_at: that one has 1105// live callers and rule 19 says add, never widen underneath them. When info/abbrev are empty this 1106// DELEGATES to the 2-section builder, so a -g build with line info only stays byte-identical to 1107// what it produced yesterday -- the property nx_nxasm_neutral_gate exists to hold. 1108// 1109// All three sections live PAST the loaded image and are described only by the section header table, 1110// which no PT_LOAD covers. That is why this cannot change runtime behaviour: a loader never reads a 1111// byte of it. DDR-002 section 5 chose that placement deliberately. 1112func x86_build_elf_dbg3_at(code: *u8, code_len: i64, entry_off: i64, out: *u8, 1113 dline: *u8, dline_len: i64, 1114 dinfo: *u8, dinfo_len: i64, 1115 dabbr: *u8, dabbr_len: i64) -> i64 { 1116 if dinfo_len <= 0 { return x86_build_elf_dbg_at(code, code_len, entry_off, out, dline, dline_len) } 1117 if dabbr_len <= 0 { return x86_build_elf_dbg_at(code, code_len, entry_off, out, dline, dline_len) } 1118 1119 let total: i64 = x86_build_elf_at(code, code_len, entry_off, out) 1120 1121 let line_off: i64 = total 1122 var i: i64 = 0 1123 while i < dline_len { out[line_off + i] = dline[i]; i = i + 1 } 1124 1125 let info_off: i64 = line_off + dline_len 1126 i = 0 1127 while i < dinfo_len { out[info_off + i] = dinfo[i]; i = i + 1 } 1128 1129 let abbr_off: i64 = info_off + dinfo_len 1130 i = 0 1131 while i < dabbr_len { out[abbr_off + i] = dabbr[i]; i = i + 1 } 1132 1133 // .shstrtab: leading NUL then the five names, each offset captured as it is written so no 1134 // index is ever computed by hand. 1135 let str_off: i64 = abbr_off + dabbr_len 1136 var so: i64 = str_off 1137 out[so] = 0 1138 so = so + 1 1139 let n_text: i64 = so - str_off 1140 so = so + x86_sh_str(out, so, ".text" as *u8) 1141 let n_line: i64 = so - str_off 1142 so = so + x86_sh_str(out, so, ".debug_line" as *u8) 1143 let n_info: i64 = so - str_off 1144 so = so + x86_sh_str(out, so, ".debug_info" as *u8) 1145 let n_abbr: i64 = so - str_off 1146 so = so + x86_sh_str(out, so, ".debug_abbrev" as *u8) 1147 let n_str: i64 = so - str_off 1148 so = so + x86_sh_str(out, so, ".shstrtab" as *u8) 1149 let str_len: i64 = so - str_off 1150 1151 var sht_off: i64 = so 1152 while (sht_off & 7) != 0 { out[sht_off] = 0; sht_off = sht_off + 1 } 1153 1154 x86_sh_ent(out, sht_off, 0, 0, 0, 0, 0, 0, 0) 1155 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ, n_text, 1, 6, 1156 X86_BASE + X86_HDRLEN, X86_HDRLEN, code_len, 16) 1157 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*2, n_line, 1, 0, 0, line_off, dline_len, 1) 1158 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*3, n_info, 1, 0, 0, info_off, dinfo_len, 1) 1159 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*4, n_abbr, 1, 0, 0, abbr_off, dabbr_len, 1) 1160 x86_sh_ent(out, sht_off + X86_SHT_ENTSZ*5, n_str, 3, 0, 0, str_off, str_len, 1) 1161 1162 x86_put_u64le(out, 40, sht_off) 1163 x86_put_u16le(out, 58, X86_SHT_ENTSZ) 1164 x86_put_u16le(out, 60, 6) // e_shnum 1165 x86_put_u16le(out, 62, 5) // e_shstrndx -> .shstrtab 1166 1167 return sht_off + X86_SHT_ENTSZ * 6 1168} 1169 1170// Convenience wrapper: entry at the first code byte (entry_off = 0). 1171func x86_build_elf(code: *u8, code_len: i64, out: *u8) -> i64 { 1172 return x86_build_elf_at(code, code_len, 0, out) 1173}