namespace WhiteMagic.Assembly; /// /// The default backend. Hand-emits calling-convention /// trampolines and injection 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 (uint[] — each 4 or 8 bytes per pointerSize). /// 4 (x86) or 8 (x64). /// Calling convention. /// 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, uint[] arguments, int pointerSize, CallConvention convention) { var buffer = new List(64); if (pointerSize == 4) { BuildX86Stub(buffer, checked((uint)stubAddress), checked((uint)targetAddress), arguments, 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 } private void BuildX64Stub(List buffer, ulong stubAddr, ulong target, uint[] args) { // Windows x64 single ABI: first 4 args in RCX, RDX, R8D, R9D. ulong current = stubAddr; var regCodes = new byte[] { 0xB9, 0xBA, 0xB8, 0xB9 }; var rexBytes = new byte[] { 0x00, 0x00, 0x41, 0x41 }; int regCount = Math.Min(args.Length, 4); for (int i = 0; i < regCount; i++) { if (rexBytes[i] != 0) buffer.Add(rexBytes[i]); buffer.Add(regCodes[i]); EmitU32(buffer, args[i]); current += (rexBytes[i] != 0 ? 6u : 5u); } // Push remaining args in reverse order for (int i = args.Length - 1; i >= 4; i--) { current += 5; buffer.Add(0x68); EmitU32(buffer, args[i]); } // call rel32 long distance = (long)target - (long)(current + 5); if (distance < int.MinValue || distance > 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); // Pop any args pushed on stack (x64 is caller-clean) int stackArgs = args.Length > 4 ? args.Length - 4 : 0; if (stackArgs > 0) { int bytes = stackArgs * 8; buffer.Add(0x48); // REX.W buffer.Add(bytes <= 127 ? (byte)0x83 : (byte)0x81); // add r/m64, imm8/imm32 buffer.Add(0xC4); // rsp if (bytes <= 127) buffer.Add((byte)bytes); else EmitU32(buffer, (uint)bytes); } 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; } }