293 lines
11 KiB
C#
293 lines
11 KiB
C#
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;
|
||
}
|
||
}
|