ARchive old spec
This commit is contained in:
@@ -46,21 +46,35 @@ public sealed class StubAssembler : IAssembler
|
||||
/// </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 (uint[] — each 4 or 8 bytes per pointerSize).</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.</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,
|
||||
uint[] arguments, int pointerSize, CallConvention convention)
|
||||
nuint[] arguments, int pointerSize, CallConvention convention)
|
||||
{
|
||||
var buffer = new List<byte>(64);
|
||||
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),
|
||||
arguments, convention);
|
||||
args32, convention);
|
||||
}
|
||||
else if (pointerSize == 8)
|
||||
{
|
||||
@@ -153,57 +167,113 @@ public sealed class StubAssembler : IAssembler
|
||||
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, uint[] args)
|
||||
ulong target, nuint[] args)
|
||||
{
|
||||
// Windows x64 single ABI: first 4 args in RCX, RDX, R8D, R9D.
|
||||
ulong current = stubAddr;
|
||||
|
||||
var regCodes = new byte[] { 0xB9, 0xBA, 0xB8, 0xB9 };
|
||||
var rexBytes = new byte[] { 0x00, 0x00, 0x41, 0x41 };
|
||||
// 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++)
|
||||
{
|
||||
if (rexBytes[i] != 0)
|
||||
buffer.Add(rexBytes[i]);
|
||||
buffer.Add(regCodes[i]);
|
||||
EmitU32(buffer, args[i]);
|
||||
current += (rexBytes[i] != 0 ? 6u : 5u);
|
||||
byte[] prefix = regMoves[i];
|
||||
buffer.Add(prefix[0]);
|
||||
buffer.Add(prefix[1]);
|
||||
EmitU64(buffer, args[i]);
|
||||
current += (uint)(prefix.Length + 8);
|
||||
}
|
||||
|
||||
// Push remaining args in reverse order
|
||||
for (int i = args.Length - 1; i >= 4; i--)
|
||||
// 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++)
|
||||
{
|
||||
current += 5;
|
||||
buffer.Add(0x68);
|
||||
EmitU32(buffer, args[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
|
||||
// 4. call rel32
|
||||
long distance = (long)target - (long)(current + 5);
|
||||
if (distance < int.MinValue || distance > int.MaxValue)
|
||||
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;
|
||||
|
||||
// Pop any args pushed on stack (x64 is caller-clean)
|
||||
int stackArgs = args.Length > 4 ? args.Length - 4 : 0;
|
||||
if (stackArgs > 0)
|
||||
{
|
||||
int bytes = stackArgs * 8;
|
||||
buffer.Add(0x48); // REX.W
|
||||
buffer.Add(bytes <= 127 ? (byte)0x83 : (byte)0x81); // add r/m64, imm8/imm32
|
||||
buffer.Add(0xC4); // rsp
|
||||
if (bytes <= 127)
|
||||
buffer.Add((byte)bytes);
|
||||
else
|
||||
EmitU32(buffer, (uint)bytes);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user