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 32-byte shadow space; 16-byte stack alignment at the call instruction. /// /// /// The stub frame: /// /// sub rsp, 0x20 ; 32-byte shadow space + restores 16-byte alignment /// mov rcx, arg0 ; 64-bit loads (REX.W mov r64, imm64) /// mov rdx, arg1 /// mov r8, arg2 /// mov r9, arg3 /// mov rax, arg[N] /// mov [rsp + 0x20 + 8*(N-4)], rax ; stack args placed above the shadow slots /// ... /// call target (rel32) /// add rsp, 0x20 /// ret /// /// On entry the stub sees rsp ≡ 8 (mod 16) (the caller's call pushed /// the return address). sub rsp, 0x20 moves rsp to ≡ 0 (mod 16). Just before /// the inner call, rsp is still ≡ 0, so target's entry rsp is /// ≡ 8 (mod 16) — no, wait: entry ≡ 0 after sub; call target pushes 8, so /// target's entry is ≡ 0 − 8 ≡ 8; but we want target entry ≡ 0. Re-check: /// Stub entry: rsp ≡ 8 (mod 16). After sub rsp, 0x20: /// 8 − 0x20 = −24 ≡ 8 (mod 16). After the inner call, target entry is /// 8 − 8 ≡ 0 (mod 16). Target is 16-byte aligned — SSE safe. /// private void BuildX64Stub(List buffer, ulong stubAddr, ulong target, nuint[] args) { ulong current = stubAddr; // 1. Allocate shadow space. // sub rsp, 0x20 ; 32 bytes = 4 shadow slots AND (entry − 0x20) ≡ 8 (mod 16), // so rsp after the sub ≡ 8 (mod 16); `call target` will push 8 and land target // at ≡ 0 (mod 16). buffer.Add(0x48); buffer.Add(0x81); buffer.Add(0xEC); // sub rsp, imm32 EmitU32(buffer, 0x20); current += 7; // 2. 64-bit register loads. // RCX = REX.W 0xB9 + imm64 (10 bytes) // RDX = REX.W 0xBA + imm64 (10 bytes) // R8 = REX.WB 0xB8 + imm64 (11 bytes, REX.W|R = 0x49) // R9 = REX.WB 0xB9 + imm64 (11 bytes) byte[][] regMoves = [ [0x48, 0xB9], // mov rcx, imm64 [0x48, 0xBA], // mov rdx, imm64 [0x49, 0xB8], // mov r8, imm64 [0x49, 0xB9], // mov r9, imm64 ]; 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); } // 3. Stack args (args 4+): placed at [rsp + 0x20 + 8*(i-4)]. // Each is two instructions: // mov rax, imm64 (10 bytes) // mov [rsp + disp], rax (5..8 bytes depending on disp8/disp32) 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; } } // 4. 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; // 5. add rsp, 0x20 ; tear down shadow space buffer.Add(0x48); buffer.Add(0x81); buffer.Add(0xC4); EmitU32(buffer, 0x20); // 6. ret 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; } }