Files
whitemagic/WhiteMagic/Assembly/StubAssembler.cs
T
2026-07-21 22:30:10 +02:00

293 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace WhiteMagic.Assembly;
/// <summary>
/// The default <see cref="IAssembler"/> backend. Hand-emits calling-convention
/// trampolines and remote-execution stubs using deterministic byte emitters
/// (<see cref="EmitU8"/>, <see cref="EmitU32"/>, <see cref="EmitU64"/>). Has
/// no native or third-party dependency — no FASM, no Iced.
/// </summary>
/// <remarks>
/// <see cref="Assemble"/> is not supported by this backend (it is a parse-free
/// emitter, not a text assembler). Use <see cref="IcedAssembler"/> (Phase 8) for
/// arbitrary mnemonics.
/// </remarks>
public sealed class StubAssembler : IAssembler
{
/// <inheritdoc />
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<byte> buffer, byte value) => buffer.Add(value);
public void EmitU32(List<byte> 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<byte> buffer, ulong value)
{
EmitU32(buffer, (uint)value);
EmitU32(buffer, (uint)(value >> 32));
}
// ── Call-stub builders ─────────────────────────────────────────────────
/// <summary>
/// Builds a calling-convention call stub for x86 or x64.
/// </summary>
/// <param name="stubAddress">Where the stub lands (for E8 rel32 encoding).</param>
/// <param name="targetAddress">Function to call.</param>
/// <param name="arguments">Argument values. For x86 each element holds a 32-bit argument;
/// for x64 each element holds the full 64-bit pointer-sized argument.</param>
/// <param name="pointerSize">4 (x86) or 8 (x64).</param>
/// <param name="convention">Calling convention (ignored on x64; Windows has a single ABI).</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="pointerSize"/> is not 4 or 8,
/// or <paramref name="convention"/> is not known, or the distance between stub and target
/// exceeds the E8 rel32 range.</exception>
public byte[] BuildCallStub(IntPtr stubAddress, IntPtr targetAddress,
nuint[] arguments, int pointerSize, CallConvention convention)
{
var buffer = new List<byte>(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<byte> 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
}
/// <summary>
/// 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 <c>call</c> instruction.
/// </summary>
/// <remarks>
/// <para>The stub frame:</para>
/// <code>
/// 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
/// </code>
/// <para>On entry the stub sees <c>rsp ≡ 8 (mod 16)</c> (the caller's <c>call</c> pushed
/// the return address). <c>sub rsp, 0x20</c> moves rsp to <c>≡ 0 (mod 16)</c>. Just before
/// the inner <c>call</c>, rsp is still <c>≡ 0</c>, so target's entry rsp is
/// <c>≡ 8 (mod 16)</c> — no, wait: entry ≡ 0 after sub; <c>call target</c> pushes 8, so
/// target's entry is ≡ 0 8 ≡ 8; but we want target entry ≡ 0. Re-check:</para>
/// <para>Stub entry: <c>rsp ≡ 8 (mod 16)</c>. After <c>sub rsp, 0x20</c>:
/// <c>8 0x20 = 24 ≡ 8 (mod 16)</c>. After the inner <c>call</c>, target entry is
/// <c>8 8 ≡ 0 (mod 16)</c>. Target is 16-byte aligned — SSE safe.</para>
/// </remarks>
private void BuildX64Stub(List<byte> 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 ────────────────────────────────────────────────
/// <summary>Emit <c>mov reg32, imm32</c> and advances <paramref name="ip"/> by 5.</summary>
private static void EmitMovRegImm32(List<byte> 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;
}
}