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;
}
}