Fix x64 stub ABI and marshal-path sizing; dedupe memory readers
x64 call stub was ABI-broken: fixed 0x20 frame left rsp misaligned at the inner call (callee entry rsp ≡ 0, ABI requires ≡ 8) and, for 5+ args, wrote stack args over the return address. Compute frame K ≡ 8 (mod 16), K ≥ 0x20 + 8*stackArgs, so the callee sees a 16-aligned stack and stack args land above the shadow window. Load register args as full 64-bit imm64 (was imm32, which truncated pointers > 4 GiB). BuildCallStub now takes nuint[]; x86 range- checks each arg against uint.MaxValue instead of silently truncating. MarshalCache conflated managed and unmanaged width in one Size field: the blittable path needs Unsafe.SizeOf<T> (bool = 1) while the marshal path needs Marshal.SizeOf<T> (inline ByValTStr/ByValArray expand past the managed pointer). Add MarshalSize; MemoryBase picks per TypeRequiresMarshal at all four IO sites. Prevents PtrToStructure/StructureToPtr from over-reading/overwriting the pinned scratch buffer (heap corruption on write). Extract shared RPM/WPM into RpmHelper: honor partial reads (dead Array.Resize removed), consistent write-return semantics; InProcessReader now guards MainModule like ExternalReader. Tests: x64 frame-alignment property + inline-marshal round-trip added (both fail against the pre-fix code); existing x64 byte-expectation tests updated to the new frame. Build clean, 100/100 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,56 +170,60 @@ public sealed class StubAssembler : IAssembler
|
||||
/// <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.
|
||||
/// above a 32-byte shadow space; 16-byte stack alignment at the inner <c>call</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The stub frame:</para>
|
||||
/// <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 0–5 args, K ∈ {0x28, 0x38}; pattern scales linearly.</item>
|
||||
/// </list>
|
||||
/// <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
|
||||
/// 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>
|
||||
/// <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);
|
||||
// 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;
|
||||
|
||||
// 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)
|
||||
// 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], // mov rcx, imm64
|
||||
[0x48, 0xBA], // mov rdx, imm64
|
||||
[0x49, 0xB8], // mov r8, imm64
|
||||
[0x49, 0xB9], // mov r9, imm64
|
||||
[0x48, 0xB9],
|
||||
[0x48, 0xBA],
|
||||
[0x49, 0xB8],
|
||||
[0x49, 0xB9],
|
||||
];
|
||||
|
||||
int regCount = Math.Min(args.Length, 4);
|
||||
@@ -232,10 +236,8 @@ public sealed class StubAssembler : IAssembler
|
||||
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)
|
||||
// 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;
|
||||
@@ -258,7 +260,7 @@ public sealed class StubAssembler : IAssembler
|
||||
}
|
||||
}
|
||||
|
||||
// 4. call rel32
|
||||
// call rel32
|
||||
long distance = (long)target - (long)(current + 5);
|
||||
if (distance is < int.MinValue or > int.MaxValue)
|
||||
{
|
||||
@@ -269,11 +271,10 @@ public sealed class StubAssembler : IAssembler
|
||||
EmitU32(buffer, (uint)distance);
|
||||
current += 5;
|
||||
|
||||
// 5. add rsp, 0x20 ; tear down shadow space
|
||||
// Tear down the frame symmetrically.
|
||||
buffer.Add(0x48); buffer.Add(0x81); buffer.Add(0xC4);
|
||||
EmitU32(buffer, 0x20);
|
||||
EmitU32(buffer, (uint)k);
|
||||
|
||||
// 6. ret
|
||||
buffer.Add(0xC3);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user