namespace WhiteMagic.Assembly; /// /// The default backend. Hand-emits calling-convention /// trampolines and remote-execution stubs using deterministic byte emitters /// (, , ). Has /// no native or third-party dependency — no FASM, no Iced. /// /// /// is not supported by this backend (it is a parse-free /// emitter, not a text assembler). Use (Phase 8) for /// arbitrary mnemonics. /// public sealed class StubAssembler : IAssembler { /// public byte[] Assemble(string assemblyText, ulong origin = 0) { throw new NotSupportedException( "StubAssembler does not parse text assembly. " + "Use IcedAssembler (Phase 8) for arbitrary mnemonics."); } // ── Emit primitives ──────────────────────────────────────────────────── public void EmitU8(List buffer, byte value) => buffer.Add(value); public void EmitU32(List buffer, uint value) { buffer.Add((byte)value); buffer.Add((byte)(value >> 8)); buffer.Add((byte)(value >> 16)); buffer.Add((byte)(value >> 24)); } public void EmitU64(List buffer, ulong value) { EmitU32(buffer, (uint)value); EmitU32(buffer, (uint)(value >> 32)); } // ── Call-stub builders ───────────────────────────────────────────────── /// /// Builds a calling-convention call stub for x86 or x64. /// /// Where the stub lands (for E8 rel32 encoding). /// Function to call. /// Argument values. For x86 each element holds a 32-bit argument; /// for x64 each element holds the full 64-bit pointer-sized argument. /// 4 (x86) or 8 (x64). /// Calling convention (ignored on x64; Windows has a single ABI). /// is not 4 or 8, /// or is not known, or the distance between stub and target /// exceeds the E8 rel32 range. public byte[] BuildCallStub(IntPtr stubAddress, IntPtr targetAddress, nuint[] arguments, int pointerSize, CallConvention convention) { var buffer = new List(96); if (pointerSize == 4) { // X86 args are 32-bit. Truncate nuint down to uint — callers must pass values // that fit in 32 bits on x86 targets. uint[] args32 = new uint[arguments.Length]; for (int i = 0; i < arguments.Length; i++) { ulong v = arguments[i]; if (v > uint.MaxValue) { throw new ArgumentOutOfRangeException(nameof(arguments), $"Argument {i} = 0x{v:X} does not fit in 32 bits (x86 target)."); } args32[i] = (uint)v; } BuildX86Stub(buffer, checked((uint)stubAddress), checked((uint)targetAddress), args32, convention); } else if (pointerSize == 8) { // Windows x64 uses a single ABI — the convention parameter is unused. BuildX64Stub(buffer, (ulong)(nint)stubAddress, (ulong)(nint)targetAddress, arguments); } else { throw new ArgumentOutOfRangeException(nameof(pointerSize), pointerSize, $"Expected 4 (x86) or 8 (x64), got {pointerSize}."); } return buffer.ToArray(); } private void BuildX86Stub(List buffer, uint stubAddr, uint target, uint[] args, CallConvention convention) { uint current = stubAddr; int argIndex = 0; switch (convention) { case CallConvention.Thiscall when args.Length - argIndex >= 1: EmitMovRegImm32(buffer, 0xB9, args[argIndex], ref current); // mov ecx, arg0 argIndex++; break; case CallConvention.Fastcall: if (args.Length - argIndex >= 1) { EmitMovRegImm32(buffer, 0xB9, args[argIndex], ref current); // mov ecx, arg0 argIndex++; } if (args.Length - argIndex >= 1) { EmitMovRegImm32(buffer, 0xBA, args[argIndex], ref current); // mov edx, arg1 argIndex++; } break; case CallConvention.Cdecl: case CallConvention.Stdcall: break; default: throw new ArgumentOutOfRangeException(nameof(convention), convention, $"Unsupported calling convention: {convention}."); } // Push remaining args in reverse order (right-to-left) for (int i = args.Length - 1; i >= argIndex; i--) { current += 5; buffer.Add(0x68); // push imm32 EmitU32(buffer, args[i]); } // call rel32 long distance = (long)target - (long)(current + 5); if (distance < int.MinValue || distance > int.MaxValue) { throw new ArgumentOutOfRangeException( $"target (0x{target:X}) is >2 GiB from stub (0x{stubAddr:X}); " + "E8 rel32 cannot encode this distance. Place the stub closer to the target."); } buffer.Add(0xE8); EmitU32(buffer, (uint)distance); current += 5; // Caller cleanup (cdecl only) int stackCount = args.Length - argIndex; if (convention == CallConvention.Cdecl && stackCount > 0) { int cleanup = stackCount * 4; if (cleanup <= 127) { buffer.Add(0x83); // add esp, imm8 buffer.Add(0xC4); buffer.Add((byte)cleanup); } else { buffer.Add(0x81); // add esp, imm32 buffer.Add(0xC4); EmitU32(buffer, (uint)cleanup); } } buffer.Add(0xC3); // ret } /// /// Builds a Windows x64 call stub that conforms to the Microsoft x64 ABI: /// first 4 integer/pointer args in RCX, RDX, R8, R9 (64-bit loads); stack args /// above a 32-byte shadow space; 16-byte stack alignment at the inner call. /// /// /// Frame derivation. The ABI requires the inner call site to /// land with call-site rsp ≡ 0 (mod 16), so that call pushes 8 bytes and the /// callee sees entry rsp ≡ 8 — the value an MSVC prologue (push rbp; sub rsp, 0x20) /// expects, and the only value for which locals land 16-aligned (SSE-safe). /// /// Stub entry: rsp ≡ 8 (mod 16). /// Need post-sub rsp ≡ 0 → sub operand K satisfies K ≡ 8 (mod 16). /// Frame must hold shadow space (0x20) + stack args (8 bytes each for args 4+). /// Choose the smallest such K: K = frameBytes + ((8 − frameBytes) mod 16 + 16) mod 16. /// For 0–5 args, K ∈ {0x28, 0x38}; pattern scales linearly. /// /// /// sub rsp, K ; K ≡ 8 (mod 16), K ≥ 0x20 + 8·stackArgs /// mov rcx, arg0 ; REX.W + imm64 (10 bytes) /// mov rdx, arg1 ; REX.W + imm64 (10 bytes) /// mov r8, arg2 ; REX.WB+ imm64 (10 bytes, REX.R) /// mov r9, arg3 ; REX.WB+ imm64 (10 bytes, REX.R) /// mov rax, arg[N] ; REX.W + imm64 (10 bytes) /// mov [rsp + 0x20 + 8*(N-4)], rax (5/8 bytes) /// call target (rel32) ( 5 bytes) /// add rsp, K ( 7 bytes) /// ret ( 1 byte) /// /// private void BuildX64Stub(List buffer, ulong stubAddr, ulong target, nuint[] args) { ulong current = stubAddr; // Compute frame size K. K ≡ 8 (mod 16) so that the inner call sees // post-sub rsp ≡ 0 and delivers target entry rsp ≡ 8 (mod 16). int stackArgs = Math.Max(0, args.Length - 4); int frameBytes = 0x20 + 8 * stackArgs; int k = frameBytes + ((8 - (frameBytes % 16) + 16) % 16); // sub rsp, imm32 (always imm32 form — constant 7 bytes regardless of K). buffer.Add(0x48); buffer.Add(0x81); buffer.Add(0xEC); EmitU32(buffer, (uint)k); current += 7; // 64-bit register loads for args 0..3. All encodings are exactly 10 bytes: // REX.W (0x48) + 0xB9 + imm64 → mov rcx, imm64 // REX.W (0x48) + 0xBA + imm64 → mov rdx, imm64 // REX.WB(0x49) + 0xB8 + imm64 → mov r8, imm64 (REX.R for r8) // REX.WB(0x49) + 0xB9 + imm64 → mov r9, imm64 (REX.R) byte[][] regMoves = [ [0x48, 0xB9], [0x48, 0xBA], [0x49, 0xB8], [0x49, 0xB9], ]; int regCount = Math.Min(args.Length, 4); for (int i = 0; i < regCount; i++) { byte[] prefix = regMoves[i]; buffer.Add(prefix[0]); buffer.Add(prefix[1]); EmitU64(buffer, args[i]); current += (uint)(prefix.Length + 8); } // Stack args: written at [post-sub-rsp + 0x20 + 8*(i-4)], i.e. above the // shadow window, where the inner call's callee expects them. for (int i = 4; i < args.Length; i++) { int offset = 0x20 + (i - 4) * 8; buffer.Add(0x48); buffer.Add(0xB8); // mov rax, imm64 EmitU64(buffer, args[i]); current += 10; buffer.Add(0x48); buffer.Add(0x89); // mov [rsp + disp], rax if (offset <= 127) { buffer.Add(0x44); buffer.Add(0x24); // ModRM: [rsp + disp8] buffer.Add((byte)offset); current += 5; } else { buffer.Add(0x84); buffer.Add(0x24); // ModRM: [rsp + disp32] EmitU32(buffer, (uint)offset); current += 8; } } // call rel32 long distance = (long)target - (long)(current + 5); if (distance is < int.MinValue or > int.MaxValue) { throw new ArgumentOutOfRangeException( "target and stub are >2 GiB apart; E8 rel32 cannot encode this distance."); } buffer.Add(0xE8); EmitU32(buffer, (uint)distance); current += 5; // Tear down the frame symmetrically. buffer.Add(0x48); buffer.Add(0x81); buffer.Add(0xC4); EmitU32(buffer, (uint)k); buffer.Add(0xC3); } // ── Instruction helpers ──────────────────────────────────────────────── /// Emit mov reg32, imm32 and advances by 5. private static void EmitMovRegImm32(List buffer, byte opcode, uint imm32, ref uint ip) { buffer.Add(opcode); buffer.Add((byte)imm32); buffer.Add((byte)(imm32 >> 8)); buffer.Add((byte)(imm32 >> 16)); buffer.Add((byte)(imm32 >> 24)); ip += 5; } }