Initial commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for relative/absolute addressing in <see cref="MemoryBase"/>.
|
||||
/// GetAbsolute(relative) = ImageBase + relative.
|
||||
/// GetRelative(absolute) = absolute - ImageBase (inverse of GetAbsolute).
|
||||
/// </summary>
|
||||
public class AddressingTests
|
||||
{
|
||||
private static ExternalReader OpenSelf()
|
||||
{
|
||||
return new ExternalReader(
|
||||
Process.GetCurrentProcess(),
|
||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAbsolute_resolves_relative_offset()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
IntPtr imageBase = reader.ImageBase;
|
||||
IntPtr result = reader.GetAbsolute((IntPtr)0x1000);
|
||||
Assert.Equal(imageBase + 0x1000, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRelative_returns_absolute_minus_image_base()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
IntPtr imageBase = reader.ImageBase;
|
||||
IntPtr absolute = imageBase + 0x2000;
|
||||
IntPtr relative = reader.GetRelative(absolute);
|
||||
Assert.Equal((IntPtr)((nint)absolute - (nint)imageBase), relative);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAbsolute_and_GetRelative_are_inverses()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
IntPtr offset = (IntPtr)0x3000;
|
||||
|
||||
// Round-trip: offset -> absolute -> back to offset
|
||||
IntPtr absolute = reader.GetAbsolute(offset);
|
||||
IntPtr back = reader.GetRelative(absolute);
|
||||
Assert.Equal(offset, back);
|
||||
|
||||
// Reverse round-trip: absolute -> offset -> back to absolute
|
||||
IntPtr relative = reader.GetRelative(absolute);
|
||||
IntPtr absoluteAgain = reader.GetAbsolute(relative);
|
||||
Assert.Equal(absolute, absoluteAgain);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRelative_on_ImageBase_returns_zero()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
IntPtr relative = reader.GetRelative(reader.ImageBase);
|
||||
Assert.Equal(IntPtr.Zero, relative);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_with_isRelative_true_uses_image_base()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
// DOS header 'MZ' at the image base
|
||||
byte firstByte = reader.Read<byte>(IntPtr.Zero, isRelative: true);
|
||||
Assert.Equal(0x4D, firstByte);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_with_isRelative_true_resolves_correctly()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr absolute = pin.AddrOfPinnedObject();
|
||||
IntPtr relative = reader.GetRelative(absolute);
|
||||
|
||||
Assert.True(reader.Write(relative, 42, isRelative: true));
|
||||
Assert.Equal(42, reader.Read<int>(absolute));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBytes_with_isRelative_true_resolves_correctly()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] data = reader.ReadBytes(IntPtr.Zero, 2, isRelative: true);
|
||||
Assert.Equal(0x4D, data[0]);
|
||||
Assert.Equal(0x5A, data[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="InProcessReader"/> — direct pointer dereference against
|
||||
/// the own process. Verifies the shared <see cref="MemoryBase"/> API works for
|
||||
/// both external and in-process readers.
|
||||
/// </summary>
|
||||
public class InProcessReaderTests
|
||||
{
|
||||
private static InProcessReader CreateReader()
|
||||
{
|
||||
return new InProcessReader();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageBase_is_nonzero()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
Assert.NotEqual(IntPtr.Zero, reader.ImageBase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_int_reads_known_value_from_own_memory()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
int expected = 0x12345678;
|
||||
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
int result = reader.Read<int>(addr);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_int_writes_and_reads_back()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, unchecked((int)0xCAFEBABE)));
|
||||
Assert.Equal(unchecked((int)0xCAFEBABE), reader.Read<int>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_bytes_reads_known_bytes()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
byte[] expected = [0x0A, 0x0B, 0x0C, 0x0D];
|
||||
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
byte[] result = reader.ReadBytes(addr, 4);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_bytes_writes_and_reads_back()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
byte[] slot = new byte[4];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
byte[] expected = [0xDE, 0xAD, 0xBE, 0xEF];
|
||||
|
||||
int written = reader.WriteBytes(addr, expected);
|
||||
Assert.Equal(4, written);
|
||||
|
||||
byte[] result = reader.ReadBytes(addr, 4);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_struct_via_InProcessReader()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
var slot = new TestStruct { X = 10, Y = 20 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
var result = reader.Read<TestStruct>(addr);
|
||||
Assert.Equal(10, result.X);
|
||||
Assert.Equal(20, result.Y);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_struct_via_InProcessReader()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
var slot = new TestStruct { X = 1, Y = 2 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, new TestStruct { X = 99, Y = 88 }));
|
||||
var result = reader.Read<TestStruct>(addr);
|
||||
Assert.Equal(99, result.X);
|
||||
Assert.Equal(88, result.Y);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_disposes_handle()
|
||||
{
|
||||
var reader = CreateReader();
|
||||
Assert.False(reader.Handle.IsClosed);
|
||||
reader.Dispose();
|
||||
Assert.True(reader.Handle.IsClosed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="MarshalCache{T}"/>: blittable size, marshal-required flag,
|
||||
/// IsIntPtr, and computed-once behavior.
|
||||
/// </summary>
|
||||
public class MarshalCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void Size_for_int_is_4()
|
||||
{
|
||||
Assert.Equal(4, MarshalCache<int>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_byte_is_1()
|
||||
{
|
||||
Assert.Equal(1, MarshalCache<byte>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_IntPtr_matches_native_pointer_size()
|
||||
{
|
||||
Assert.Equal(IntPtr.Size, MarshalCache<IntPtr>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_bool_is_1()
|
||||
{
|
||||
Assert.Equal(1, MarshalCache<bool>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_enum_matches_underlying_type()
|
||||
{
|
||||
Assert.Equal(4, MarshalCache<DayOfWeek>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_blittable_struct_is_accurate()
|
||||
{
|
||||
Assert.Equal(8, MarshalCache<BlittableStruct>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeRequiresMarshal_is_false_for_blittable_types()
|
||||
{
|
||||
Assert.False(MarshalCache<int>.TypeRequiresMarshal);
|
||||
Assert.False(MarshalCache<long>.TypeRequiresMarshal);
|
||||
Assert.False(MarshalCache<BlittableStruct>.TypeRequiresMarshal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeRequiresMarshal_is_true_for_types_with_MarshalAs_field()
|
||||
{
|
||||
Assert.True(MarshalCache<MarshalAsStruct>.TypeRequiresMarshal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsIntPtr_is_true_for_IntPtr()
|
||||
{
|
||||
Assert.True(MarshalCache<IntPtr>.IsIntPtr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsIntPtr_is_false_for_non_IntPtr_types()
|
||||
{
|
||||
Assert.False(MarshalCache<int>.IsIntPtr);
|
||||
Assert.False(MarshalCache<long>.IsIntPtr);
|
||||
Assert.False(MarshalCache<BlittableStruct>.IsIntPtr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_properties_are_computed_once_and_cached()
|
||||
{
|
||||
int size1 = MarshalCache<int>.Size;
|
||||
bool marshal1 = MarshalCache<int>.TypeRequiresMarshal;
|
||||
bool intPtr1 = MarshalCache<int>.IsIntPtr;
|
||||
|
||||
int size2 = MarshalCache<int>.Size;
|
||||
bool marshal2 = MarshalCache<int>.TypeRequiresMarshal;
|
||||
bool intPtr2 = MarshalCache<int>.IsIntPtr;
|
||||
|
||||
Assert.Equal(size1, size2);
|
||||
Assert.Equal(marshal1, marshal2);
|
||||
Assert.Equal(intPtr1, intPtr2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SizeU_matches_Size_as_uint()
|
||||
{
|
||||
Assert.Equal((uint)MarshalCache<int>.Size, MarshalCache<int>.SizeU);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BlittableStruct
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MarshalAsStruct
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
|
||||
public byte[] Data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="MemoryBase"/> abstract contract and <see cref="ExternalReader"/>
|
||||
/// round-trip (Read<T>/Write<T>, arrays) using the current process as target.
|
||||
/// </summary>
|
||||
public class MemoryBaseTests
|
||||
{
|
||||
private static ExternalReader OpenSelf()
|
||||
{
|
||||
return new ExternalReader(
|
||||
Process.GetCurrentProcess(),
|
||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageBase_is_nonzero_for_self()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.NotEqual(IntPtr.Zero, reader.ImageBase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_int_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, 0x1BADB002));
|
||||
Assert.Equal(0x1BADB002, reader.Read<int>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_byte_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, (byte)0xAB));
|
||||
Assert.Equal(0xAB, reader.Read<byte>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_long_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
long slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, unchecked((long)0xDEADBEEF_CAFEBABE)));
|
||||
Assert.Equal(unchecked((long)0xDEADBEEF_CAFEBABE), reader.Read<long>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_blittable_struct_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
var slot = new TestStruct { X = 42, Y = 99 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, new TestStruct { X = 100, Y = 200 }));
|
||||
var result = reader.Read<TestStruct>(addr);
|
||||
Assert.Equal(100, result.X);
|
||||
Assert.Equal(200, result.Y);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_bytes_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] buffer = new byte[16];
|
||||
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
byte[] expected = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
|
||||
int written = reader.WriteBytes(addr, expected);
|
||||
Assert.Equal(expected.Length, written);
|
||||
|
||||
byte[] actual = reader.ReadBytes(addr, expected.Length);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_int_array_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
int[] buffer = new int[4];
|
||||
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
int[] expected = [10, 20, 30, 40];
|
||||
|
||||
Assert.True(reader.Write(addr, expected));
|
||||
int[] actual = reader.Read<int>(addr, 4);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_struct_array_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
var buffer = new TestStruct[4];
|
||||
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
var expected = new[]
|
||||
{
|
||||
new TestStruct { X = 1, Y = 2 },
|
||||
new TestStruct { X = 3, Y = 4 },
|
||||
new TestStruct { X = 5, Y = 6 },
|
||||
new TestStruct { X = 7, Y = 8 },
|
||||
};
|
||||
|
||||
Assert.True(reader.Write(addr, expected));
|
||||
var actual = reader.Read<TestStruct>(addr, 4);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_returns_false_for_invalid_address()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.False(reader.Write(IntPtr.Zero, 42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_closes_handle()
|
||||
{
|
||||
var reader = OpenSelf();
|
||||
Assert.False(reader.Handle.IsClosed);
|
||||
reader.Dispose();
|
||||
Assert.True(reader.Handle.IsClosed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Double_dispose_does_not_throw()
|
||||
{
|
||||
var reader = OpenSelf();
|
||||
reader.Dispose();
|
||||
reader.Dispose();
|
||||
}
|
||||
|
||||
// ── Graceful failure on invalid addresses ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Read_int_on_invalid_address_returns_default()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.Equal(0, reader.Read<int>(IntPtr.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_struct_on_invalid_address_returns_default()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
var result = reader.Read<TestStruct>(IntPtr.Zero);
|
||||
Assert.Equal(0, result.X);
|
||||
Assert.Equal(0, result.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_int_array_on_invalid_address_returns_empty()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.Empty(reader.Read<int>(IntPtr.Zero, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_bytes_on_invalid_address_returns_empty()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.Empty(reader.ReadBytes(IntPtr.Zero, 10));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple blittable struct for use in tests.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TestStruct : IEquatable<TestStruct>
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
|
||||
public bool Equals(TestStruct other) => X == other.X && Y == other.Y;
|
||||
public override bool Equals(object? obj) => obj is TestStruct other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(X, Y);
|
||||
public override string ToString() => $"({X}, {Y})";
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the edge-case defects found in the second review pass:
|
||||
/// char sizing, reference-containing structs, unchecked counts, the ReadString
|
||||
/// partial-chunk skip, and ExternalReader construction against awkward targets.
|
||||
/// Each test fails against the pre-fix code.
|
||||
/// </summary>
|
||||
public class MemoryHardeningTests
|
||||
{
|
||||
private static ExternalReader OpenSelf()
|
||||
{
|
||||
return new ExternalReader(
|
||||
Process.GetCurrentProcess(),
|
||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
}
|
||||
|
||||
// ── char sizing (MarshalCache / MemoryBase.Read<char>) ──────────────────
|
||||
|
||||
[Fact]
|
||||
public void MarshalCache_char_size_is_two_bytes()
|
||||
{
|
||||
Assert.Equal(2, MarshalCache<char>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_char_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
char slot = '\0';
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, 'Z'));
|
||||
Assert.Equal('Z', reader.Read<char>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_char_array_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
char[] slot = new char[4];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
char[] expected = ['w', 'o', 'w', '!'];
|
||||
Assert.True(reader.Write(addr, expected));
|
||||
Assert.Equal(expected, reader.Read<char>(addr, 4));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
// ── reference-containing structs route to the marshal path ──────────────
|
||||
|
||||
[Fact]
|
||||
public void MarshalCache_flags_struct_with_reference_field_as_marshal_required()
|
||||
{
|
||||
// Has a string field but no [MarshalAs]; the blittable path (MemoryMarshal.Read)
|
||||
// throws for a reference-containing T, so the cache must route it to the marshal
|
||||
// path. A struct with a managed reference cannot be pinned, so the routing flag —
|
||||
// not a live round-trip — is the regression guard here.
|
||||
Assert.True(MarshalCache<StructWithReference>.TypeRequiresMarshal);
|
||||
}
|
||||
|
||||
// ── unchecked count guards ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Read_array_with_negative_count_throws_argument_out_of_range()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
int dummy = 0;
|
||||
GCHandle pin = GCHandle.Alloc(dummy, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => reader.Read<int>(pin.AddrOfPinnedObject(), -1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_array_with_zero_count_returns_empty()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
int dummy = 0;
|
||||
GCHandle pin = GCHandle.Alloc(dummy, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
Assert.Empty(reader.Read<int>(pin.AddrOfPinnedObject(), 0));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
// ── ReadString partial-chunk advance ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ReadString_advances_by_actual_bytes_when_reads_are_partial()
|
||||
{
|
||||
// The reader serves at most 3 bytes per call. The string is longer than one
|
||||
// chunk with the terminator well past it. If ReadString advanced by the
|
||||
// requested count instead of the bytes actually returned, it would skip
|
||||
// data and truncate the result.
|
||||
byte[] data = Encoding.ASCII.GetBytes("ABCDEFGHIJ\0");
|
||||
var reader = new PartialReader(data, maxChunk: 3);
|
||||
|
||||
string result = reader.ReadString(IntPtr.Zero, Encoding.ASCII, maxLength: 64);
|
||||
|
||||
Assert.Equal("ABCDEFGHIJ", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadString_stops_at_null_across_partial_chunks()
|
||||
{
|
||||
byte[] data = Encoding.ASCII.GetBytes("hi\0garbage");
|
||||
var reader = new PartialReader(data, maxChunk: 1);
|
||||
|
||||
string result = reader.ReadString(IntPtr.Zero, Encoding.ASCII, maxLength: 64);
|
||||
|
||||
Assert.Equal("hi", result);
|
||||
}
|
||||
|
||||
// ── ExternalReader construction ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ExternalReader_opens_self_with_default_access()
|
||||
{
|
||||
// The default access set must be small enough to open a normal process.
|
||||
using var reader = new ExternalReader(Process.GetCurrentProcess());
|
||||
Assert.False(reader.Handle.IsInvalid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="MemoryBase"/> that serves bytes from an in-memory buffer and
|
||||
/// caps every read to <c>maxChunk</c> bytes, to exercise partial-read handling.
|
||||
/// The address is treated as a zero-based index into the buffer.
|
||||
/// </summary>
|
||||
private sealed class PartialReader(byte[] data, int maxChunk) : MemoryBase
|
||||
{
|
||||
public override IntPtr ImageBase => IntPtr.Zero;
|
||||
public override SafeMemoryHandle Handle => null!;
|
||||
|
||||
public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
|
||||
{
|
||||
int start = (int)address;
|
||||
if (start < 0 || start >= data.Length || count <= 0)
|
||||
return [];
|
||||
int n = Math.Min(Math.Min(count, maxChunk), data.Length - start);
|
||||
return data[start..(start + n)];
|
||||
}
|
||||
|
||||
public override int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A struct that carries a managed reference. <see cref="RuntimeHelpers.IsReferenceOrContainsReferences{T}"/>
|
||||
/// reports <see langword="true"/>, so it cannot travel the blittable read path.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct StructWithReference
|
||||
{
|
||||
public int Id;
|
||||
public string Name;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests that exercise the P/Invoke surface against the current
|
||||
/// process. They prove the marshalling signatures are correct end-to-end.
|
||||
/// </summary>
|
||||
public class NativeSurfaceTests
|
||||
{
|
||||
private static SafeMemoryHandle OpenSelf(ProcessAccess access)
|
||||
{
|
||||
SafeMemoryHandle handle = NativeMethods.OpenProcess(access, false, Environment.ProcessId);
|
||||
Assert.False(handle.IsInvalid, $"OpenProcess failed: {Marshal.GetLastPInvokeError()}");
|
||||
return handle;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenProcess_on_self_returns_valid_handle_and_closes_on_dispose()
|
||||
{
|
||||
SafeMemoryHandle handle = OpenSelf(ProcessAccess.QueryInformation);
|
||||
Assert.False(handle.IsClosed);
|
||||
handle.Dispose();
|
||||
Assert.True(handle.IsClosed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadProcessMemory_reads_a_known_value_from_own_memory()
|
||||
{
|
||||
int value = 0x1BADB002;
|
||||
GCHandle pin = GCHandle.Alloc(value, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmRead | ProcessAccess.QueryInformation);
|
||||
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
||||
|
||||
bool ok = NativeMethods.ReadProcessMemory(
|
||||
handle, pin.AddrOfPinnedObject(), buffer, buffer.Length, out nint read);
|
||||
|
||||
Assert.True(ok, $"ReadProcessMemory failed: {Marshal.GetLastPInvokeError()}");
|
||||
Assert.Equal(sizeof(int), (int)read);
|
||||
Assert.Equal(value, BitConverter.ToInt32(buffer));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteProcessMemory_writes_a_value_into_own_memory()
|
||||
{
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
using SafeMemoryHandle handle = OpenSelf(
|
||||
ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
ReadOnlySpan<byte> payload = BitConverter.GetBytes(0x5EED);
|
||||
|
||||
bool ok = NativeMethods.WriteProcessMemory(
|
||||
handle, pin.AddrOfPinnedObject(), payload, payload.Length, out nint written);
|
||||
|
||||
Assert.True(ok, $"WriteProcessMemory failed: {Marshal.GetLastPInvokeError()}");
|
||||
Assert.Equal(payload.Length, (int)written);
|
||||
Assert.Equal(0x5EED, Marshal.ReadInt32(pin.AddrOfPinnedObject()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VirtualAllocEx_commits_then_protects_then_frees()
|
||||
{
|
||||
using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
|
||||
IntPtr region = NativeMethods.VirtualAllocEx(
|
||||
handle, IntPtr.Zero, 0x1000,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ReadWrite);
|
||||
Assert.NotEqual(IntPtr.Zero, region);
|
||||
|
||||
bool protect = NativeMethods.VirtualProtectEx(
|
||||
handle, region, 0x1000, MemoryProtectionType.ExecuteReadWrite, out MemoryProtectionType old);
|
||||
Assert.True(protect, $"VirtualProtectEx failed: {Marshal.GetLastPInvokeError()}");
|
||||
Assert.Equal(MemoryProtectionType.ReadWrite, old);
|
||||
|
||||
bool free = NativeMethods.VirtualFreeEx(handle, region, 0, MemoryFreeType.Release);
|
||||
Assert.True(free, $"VirtualFreeEx failed: {Marshal.GetLastPInvokeError()}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadLibrary_then_GetProcAddress_resolves_an_export()
|
||||
{
|
||||
IntPtr module = NativeMethods.LoadLibrary("kernel32.dll");
|
||||
Assert.NotEqual(IntPtr.Zero, module);
|
||||
|
||||
IntPtr proc = NativeMethods.GetProcAddress(module, "CloseHandle");
|
||||
Assert.NotEqual(IntPtr.Zero, proc);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(Context32), 716)]
|
||||
[InlineData(typeof(Context64), 1232)]
|
||||
public void Thread_context_struct_has_the_exact_native_size(Type contextType, int expectedSize)
|
||||
{
|
||||
Assert.Equal(expectedSize, Marshal.SizeOf(contextType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="MemoryBase.ReadString"/> and <see cref="MemoryBase.WriteString"/>
|
||||
/// with encoding, null-terminator stop, and max-length behavior.
|
||||
/// </summary>
|
||||
public class StringReadWriteTests
|
||||
{
|
||||
private static ExternalReader OpenSelf()
|
||||
{
|
||||
return new ExternalReader(
|
||||
Process.GetCurrentProcess(),
|
||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_ascii_then_ReadString_round_trips()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[64];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.WriteString(addr, "hello", Encoding.ASCII));
|
||||
string result = reader.ReadString(addr, Encoding.ASCII);
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_utf8_then_ReadString_round_trips()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[64];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.WriteString(addr, "héllo wörld", Encoding.UTF8));
|
||||
string result = reader.ReadString(addr, Encoding.UTF8);
|
||||
Assert.Equal("héllo wörld", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_unicode_then_ReadString_round_trips()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[128];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.WriteString(addr, "Hello\u00A9\u00AE\u20AC", Encoding.Unicode));
|
||||
string result = reader.ReadString(addr, Encoding.Unicode);
|
||||
Assert.Equal("Hello\u00A9\u00AE\u20AC", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadString_stops_at_null_terminator()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = Encoding.ASCII.GetBytes("hello\0world");
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 64);
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadString_respects_max_length()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = Encoding.ASCII.GetBytes("hello world this is a test");
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 5);
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_appends_null_terminator_automatically()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[32];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
|
||||
// Write without terminator
|
||||
Assert.True(reader.WriteString(addr, "test", Encoding.ASCII));
|
||||
|
||||
// The written bytes should end with \0
|
||||
byte[] read = reader.ReadBytes(addr, 8);
|
||||
Assert.Equal((byte)'t', read[0]);
|
||||
Assert.Equal((byte)'e', read[1]);
|
||||
Assert.Equal((byte)'s', read[2]);
|
||||
Assert.Equal((byte)'t', read[3]);
|
||||
Assert.Equal(0, read[4]); // null terminator
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadString_empty_buffer_returns_empty_string()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[1] { 0 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 1);
|
||||
Assert.Equal("", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_empty_string_writes_only_null()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[8];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
|
||||
// Write a marker first
|
||||
reader.WriteBytes(addr, [0xAB, 0xCD, 0xEF, 0x00]);
|
||||
// Now overwrite with empty string
|
||||
Assert.True(reader.WriteString(addr, "", Encoding.ASCII));
|
||||
|
||||
byte[] read = reader.ReadBytes(addr, 4);
|
||||
Assert.Equal(0, read[0]); // null
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using WhiteMagic.Assembly;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
public class StubAssemblerTests
|
||||
{
|
||||
private static StubAssembler Create() => new();
|
||||
|
||||
// ── Emit primitives ────────────────────────────────────────────────────
|
||||
|
||||
[Fact] public void EmitU8_appends_a_single_byte() { var s=Create(); var b=new List<byte>(); s.EmitU8(b,0xAB); Assert.Equal([0xAB],b); }
|
||||
[Fact] public void EmitU32_appends_little_endian() { var s=Create(); var b=new List<byte>(); s.EmitU32(b,0x11223344); Assert.Equal([0x44,0x33,0x22,0x11],b); }
|
||||
[Fact] public void EmitU32_appends_zero() { var s=Create(); var b=new List<byte>(); s.EmitU32(b,0); Assert.Equal([0,0,0,0],b); }
|
||||
[Fact] public void EmitU64_appends_little_endian() { var s=Create(); var b=new List<byte>(); s.EmitU64(b,0x1122334455667788); Assert.Equal([0x88,0x77,0x66,0x55,0x44,0x33,0x22,0x11],b); }
|
||||
[Fact] public void EmitU64_appends_high_bits() { var s=Create(); var b=new List<byte>(); s.EmitU64(b,0xDEADBEEF_CAFEBABE); Assert.Equal([0xBE,0xBA,0xFE,0xCA,0xEF,0xBE,0xAD,0xDE],b); }
|
||||
[Fact] public void StubAssembler_is_IAssembler() { Assert.IsAssignableFrom<IAssembler>(Create()); }
|
||||
[Fact] public void Assemble_throws() { Assert.Throws<NotSupportedException>(()=>Create().Assemble("nop",0)); }
|
||||
|
||||
// ── x86 cdecl ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Cdecl_0args()
|
||||
{
|
||||
uint r = 0x12345678u-(0x10000000u+5);
|
||||
Assert.Equal([0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cdecl_1arg()
|
||||
{
|
||||
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
|
||||
Assert.Equal([
|
||||
0x68,0xDD,0xCC,0xBB,0xAA,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
|
||||
0x83,0xC4,0x04,0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAABBCCDD],4,CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cdecl_2args()
|
||||
{
|
||||
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
|
||||
Assert.Equal([
|
||||
0x68,0x22,0x22,0x22,0x22,
|
||||
0x68,0x11,0x11,0x11,0x11,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
|
||||
0x83,0xC4,0x08,0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222],4,CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
// ── x86 stdcall ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Stdcall_1arg()
|
||||
{
|
||||
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
|
||||
Assert.Equal([
|
||||
0x68,0xDD,0xCC,0xBB,0xAA,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAABBCCDD],4,CallConvention.Stdcall));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stdcall_2args()
|
||||
{
|
||||
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
|
||||
Assert.Equal([
|
||||
0x68,0x22,0x22,0x22,0x22,
|
||||
0x68,0x11,0x11,0x11,0x11,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222],4,CallConvention.Stdcall));
|
||||
}
|
||||
|
||||
// ── x86 thiscall ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Thiscall_ecx_then_stack()
|
||||
{
|
||||
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
|
||||
Assert.Equal([
|
||||
0xB9,0x55,0x55,0xAA,0xAA,
|
||||
0x68,0x66,0x66,0xBB,0xBB,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAAAA5555,0xBBBB6666],4,CallConvention.Thiscall));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Thiscall_1arg_ecx_only()
|
||||
{
|
||||
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
|
||||
byte[] s=Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xCAFEBABE],4,CallConvention.Thiscall);
|
||||
Assert.Equal(11,s.Length); Assert.Equal(0xB9,s[0]); Assert.Equal(0xCAFEBABE,BitConverter.ToUInt32(s,1));
|
||||
Assert.Equal(0xE8,s[5]); Assert.Equal(r,BitConverter.ToUInt32(s,6)); Assert.Equal(0xC3,s[10]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Thiscall_0args_throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Thiscall));
|
||||
}
|
||||
|
||||
// ── x86 fastcall ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fastcall_ecx_edx_stack()
|
||||
{
|
||||
uint ca=0x10000000u+15,r=0x12345678u-(ca+5);
|
||||
Assert.Equal([
|
||||
0xB9,0x11,0x11,0x11,0x11,
|
||||
0xBA,0x22,0x22,0x22,0x22,
|
||||
0x68,0x33,0x33,0x33,0x33,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222,0x33333333],4,CallConvention.Fastcall));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fastcall_2args_registers_only()
|
||||
{
|
||||
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
|
||||
byte[] s=Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAAAAAAAA,0xBBBBBBBB],4,CallConvention.Fastcall);
|
||||
Assert.Equal(16,s.Length); Assert.Equal(0xB9,s[0]); Assert.Equal(0xAAAAAAAA,BitConverter.ToUInt32(s,1));
|
||||
Assert.Equal(0xBA,s[5]); Assert.Equal(0xBBBBBBBB,BitConverter.ToUInt32(s,6));
|
||||
Assert.Equal(0xE8,s[10]); Assert.Equal(r,BitConverter.ToUInt32(s,11)); Assert.Equal(0xC3,s[15]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fastcall_0args_is_valid()
|
||||
{
|
||||
uint r=0x12345678u-(0x10000000u+5);
|
||||
Assert.Equal([0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Fastcall));
|
||||
}
|
||||
|
||||
// ── x64 ─────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void X64_0args()
|
||||
{
|
||||
var s=Create(); ulong a=0x100000000,t=0x123456788;
|
||||
uint r=(uint)(t-(a+5));
|
||||
byte[] stub=s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[],8,CallConvention.Cdecl);
|
||||
Assert.Equal(6,stub.Length); Assert.Equal(0xE8,stub[0]); Assert.Equal(r,BitConverter.ToUInt32(stub,1)); Assert.Equal(0xC3,stub[5]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X64_1arg_mov_ecx()
|
||||
{
|
||||
var s=Create(); ulong a=0x100000000,t=0x123456788;
|
||||
uint r=(uint)(t-(a+5+5));
|
||||
Assert.Equal([
|
||||
0xB9,0xDD,0xCC,0xBB,0xAA,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[0xAABBCCDD],8,CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X64_4args_rcx_rdx_r8_r9()
|
||||
{
|
||||
var s=Create(); ulong a=0x100000000,t=0x123456788;
|
||||
uint ca=(uint)a+5+5+6+6,r=(uint)(t-(ca+5));
|
||||
Assert.Equal([
|
||||
0xB9,0x11,0x11,0x11,0x11, 0xBA,0x22,0x22,0x22,0x22,
|
||||
0x41,0xB8,0x33,0x33,0x33,0x33, 0x41,0xB9,0x44,0x44,0x44,0x44,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
|
||||
s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[0x11111111,0x22222222,0x33333333,0x44444444],8,CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X64_5args_push_cleanup()
|
||||
{
|
||||
var s=Create(); ulong a=0x100000000,t=0x123456788;
|
||||
uint ca=(uint)a+5+5+6+6+5,r=(uint)(t-(ca+5));
|
||||
Assert.Equal([
|
||||
0xB9,1,0,0,0, 0xBA,2,0,0,0,
|
||||
0x41,0xB8,3,0,0,0, 0x41,0xB9,4,0,0,0,
|
||||
0x68,5,0,0,0,
|
||||
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
|
||||
0x48,0x83,0xC4,8, 0xC3],
|
||||
s.BuildCallStub((IntPtr)(nint)a,(IntPtr)(nint)t,[1,2,3,4,5],8,CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
// ── Edge cases ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Far_target_throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Create().BuildCallStub(IntPtr.Zero, unchecked((IntPtr)(nint)0xC0000000), [], 4, CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Many_args_cleanup_uses_imm32_form()
|
||||
{
|
||||
var args = new uint[33];
|
||||
for (int i = 0; i < 33; i++) args[i] = (uint)(i * 0x10000 + i);
|
||||
byte[] stub = Create().BuildCallStub(
|
||||
(IntPtr)0x10000000, (IntPtr)0x12345678, args, 4, CallConvention.Cdecl);
|
||||
|
||||
for (int i = 0; i < stub.Length - 5; i++)
|
||||
{
|
||||
if (stub[i] == 0x81 && stub[i + 1] == 0xC4)
|
||||
{
|
||||
Assert.Equal(132, BitConverter.ToInt32(stub, i + 2));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Assert.Fail("Expected 0x81 0xC4 (add esp, imm32) not found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Invalid_pointerSize_throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x12345678, [], 2, CallConvention.Cdecl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Invalid_calling_convention_throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x12345678, [], 4, (CallConvention)99));
|
||||
}
|
||||
|
||||
// ── No-FASM ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void No_fasm_reference_in_output()
|
||||
{
|
||||
var asm = typeof(StubAssembler).Assembly;
|
||||
var refs = asm.GetReferencedAssemblies();
|
||||
Assert.DoesNotContain(refs, r =>
|
||||
r.Name!.Contains("Fasm", StringComparison.OrdinalIgnoreCase) ||
|
||||
r.Name!.Contains("ManagedFasm", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WhiteMagic\WhiteMagic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user