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}