Files
whitemagic/WhiteMagic/Assembly/StubAssembler.cs
T
kbe 040a51bf03 fix: Build documentation at compilation
Some documentation was broken. I refactored it to be declarative now the
project build correctly and produce XML documentation.
2026-07-22 19:58:53 +02:00

311 lines
13 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
{
/// <summary>
/// The maximum number of call arguments a single stub may pass. Far above any real
/// calling convention; exists only to keep frame-size and stack-offset math from
/// overflowing <see cref="int"/>.
/// </summary>
public const int MaxArguments = 256;
/// <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 ────────────────────────────────────────────────────
/// <summary>Emits a single byte into the buffer.</summary>
public void EmitU8(List<byte> buffer, byte value) => buffer.Add(value);
/// <summary>Emits a 32-bit little-endian integer into the buffer.</summary>
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));
}
/// <summary>Emits a 64-bit little-endian integer into the buffer.</summary>
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 <paramref name="arguments"/> exceeds
/// <see cref="MaxArguments"/>, 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)
{
ArgumentNullException.ThrowIfNull(arguments);
// Bound the argument count. No real calling convention passes anywhere near this
// many; the cap keeps the frame-size and stack-offset arithmetic (0x20 + 8*stackArgs)
// well inside int range, so it can never overflow into a bogus/negative frame.
ArgumentOutOfRangeException.ThrowIfGreaterThan(arguments.Length, MaxArguments, nameof(arguments));
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 a 32-byte shadow space; 16-byte stack alignment at the inner <c>call</c>.
/// </summary>
/// <remarks>
/// <para>Frame derivation. The ABI requires the <em>inner</em> <c>call</c> site to
/// land with call-site rsp ≡ 0 (mod 16), so that <c>call</c> pushes 8 bytes and the
/// callee sees entry rsp ≡ 8 — the value an MSVC prologue (<c>push rbp; sub rsp, 0x20</c>)
/// expects, and the only value for which locals land 16-aligned (SSE-safe).</para>
/// <list type="bullet">
/// <item>Stub entry: rsp ≡ 8 (mod 16).</item>
/// <item>Need post-sub rsp ≡ 0 → sub operand K satisfies K ≡ 8 (mod 16).</item>
/// <item>Frame must hold shadow space (0x20) + stack args (8 bytes each for args 4+).
/// Choose the smallest such K: <c>K = frameBytes + ((8 frameBytes) mod 16 + 16) mod 16</c>.
/// For 05 args, K ∈ {0x28, 0x38}; pattern scales linearly.</item>
/// </list>
/// <code>
/// sub rsp, K ; K ≡ 8 (mod 16), K ≥ 0x20 + 8·stackArgs
/// mov rcx, arg0 ; REX.W + imm64 (10 bytes)
/// mov rdx, arg1 ; REX.W + imm64 (10 bytes)
/// mov r8, arg2 ; REX.WB+ imm64 (10 bytes, REX.R)
/// mov r9, arg3 ; REX.WB+ imm64 (10 bytes, REX.R)
/// mov rax, arg[N] ; REX.W + imm64 (10 bytes)
/// mov [rsp + 0x20 + 8*(N-4)], rax (5/8 bytes)
/// call target (rel32) ( 5 bytes)
/// add rsp, K ( 7 bytes)
/// ret ( 1 byte)
/// </code>
/// </remarks>
private void BuildX64Stub(List<byte> buffer, ulong stubAddr,
ulong target, nuint[] args)
{
ulong current = stubAddr;
// Compute frame size K. K ≡ 8 (mod 16) so that the inner call sees
// post-sub rsp ≡ 0 and delivers target entry rsp ≡ 8 (mod 16).
int stackArgs = Math.Max(0, args.Length - 4);
int frameBytes = 0x20 + 8 * stackArgs;
int k = frameBytes + ((8 - (frameBytes % 16) + 16) % 16);
// sub rsp, imm32 (always imm32 form — constant 7 bytes regardless of K).
buffer.Add(0x48); buffer.Add(0x81); buffer.Add(0xEC);
EmitU32(buffer, (uint)k);
current += 7;
// 64-bit register loads for args 0..3. All encodings are exactly 10 bytes:
// REX.W (0x48) + 0xB9 + imm64 → mov rcx, imm64
// REX.W (0x48) + 0xBA + imm64 → mov rdx, imm64
// REX.WB(0x49) + 0xB8 + imm64 → mov r8, imm64 (REX.R for r8)
// REX.WB(0x49) + 0xB9 + imm64 → mov r9, imm64 (REX.R)
byte[][] regMoves =
[
[0x48, 0xB9],
[0x48, 0xBA],
[0x49, 0xB8],
[0x49, 0xB9],
];
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);
}
// Stack args: written at [post-sub-rsp + 0x20 + 8*(i-4)], i.e. above the
// shadow window, where the inner call's callee expects them.
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;
}
}
// 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;
// Tear down the frame symmetrically.
buffer.Add(0x48); buffer.Add(0x81); buffer.Add(0xC4);
EmitU32(buffer, (uint)k);
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;
}
}