Files
whitemagic/docs/memory-access.md
kbe 6300bebe33 Fix API documentation and examples; add comprehensive documentation suite
- Example5: Fix format string bugs (alignment specifier placement, SafeMemoryHandle formatting)
- Example4: Fix DetourManager.Detour() → Create() in all doc strings
- Example3: Fix MemoryBase.CreateFunction() → InProcessInvoker.CreateFunction() in doc strings
- Examples 1-5: Correct all runtime errors and API mismatches vs actual WhiteMagic API
- README: Fix DetourManager.Detour() examples to use Create(); add missing code fence markers
- Add WhiteMagic.Examples project with 5 comprehensive example files (40+ sub-examples)
- Add docfx.json and toc.md for DocFX API reference generation
- Add 5 conceptual guides: architecture, memory-access, execution-models, hooking, troubleshooting
- Ensure zero errors, zero warnings across all projects (net8.0-windows)

Doc strings now teach correct APIs; runtime format bugs eliminated; build succeeds.
2026-07-22 22:26:07 +02:00

12 KiB

Memory Access in WhiteMagic

WhiteMagic provides a dual memory-access model through an abstract MemoryBase class, supporting both external (out-of-process) and in-process readers with optimized typed I/O.

MemoryBase Architecture

Abstract Interface

MemoryBase defines the contract for all memory operations:

public abstract class MemoryBase : IDisposable
{
    // Abstract raw I/O
    public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false);
    public abstract int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false);
    
    // Typed I/O
    public T Read<T>(IntPtr address, bool isRelative = false) where T : struct;
    public bool Write<T>(IntPtr address, T value, bool isRelative = false) where T : struct;
    
    // Array I/O
    public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct;
    public bool Write<T>(IntPtr address, T[] values, bool isRelative = false) where T : struct;
    
    // String I/O
    public string ReadString(IntPtr address, Encoding encoding, int maxLength = 512);
    public bool WriteString(IntPtr address, string value, Encoding encoding);
}

Concrete Implementations

ExternalReader

Mechanism: ReadProcessMemory/WriteProcessMemory via P/Invoke Handle: SafeMemoryHandle to target process Use case: Primary mode for automation hosts

using var magic = Magic.Open(targetProcess);
// Uses ExternalReader internally

InProcessReader

Mechanism: ReadProcessMemory/WriteProcessMemory on self-handle Handle: SafeMemoryHandle to current process Use case: Injected code, delegate calls, detours

Design decision: Uses RPM-on-self instead of unsafe direct pointer deref because .NET cannot catch AccessViolationException, so a bad deref kills the host with no soft-failure path. The in-process speed win moves to the delegate-call and detour paths, not the reader.

using var magic = Magic.OpenInProcess();
// Uses InProcessReader internally

Typed I/O with MarshalCache

Performance Optimization

MarshalCache<T> eliminates per-call reflection overhead by caching type metadata once:

public static class MarshalCache<T>
{
    // Cached at static-constructor time
    public static readonly int Size;              // Managed blittable width
    public static readonly int MarshalSize;       // Unmanaged interop width
    public static readonly bool TypeRequiresMarshal;  // Needs PtrToStructure
    public static readonly bool IsIntPtr;         // Special handling for IntPtr
}

Blittable vs Marshalled Types

Blittable types (no marshaling needed):

  • Primitives: int, byte, float, double, bool (1 byte managed)
  • Enums (if underlying type is blittable)
  • Structs containing only blittable fields
  • Performance: MemoryMarshal.Read<T> — zero allocation

Marshalled types (require Marshal.PtrToStructure):

  • bool (4 bytes in Win32 interop)
  • char (2 bytes ANSI marshaling)
  • Structs with [MarshalAs] attributes
  • Structs with inline ByValTStr/ByValArray
  • Performance: Marshal.PtrToStructure — allocates temporary copy

Size vs MarshalSize

For most types, Size == MarshalSize. They differ when:

// Example: bool field
struct MyStruct
{
    public bool Flag;  // 1 byte managed, 4 bytes Win32 BOOL
    public int Value;
}

MarshalCache<MyStruct>.Size == 5;        // Managed layout
MarshalCache<MyStruct>.MarshalSize == 8; // Win32 BOOL is 4 bytes

Usage

using var magic = Magic.Open(process);

// Blittable read (fast path)
int health = magic.Memory.Read<int>(address);

// Marshalled read (slow path)
var gameState = magic.Memory.Read<MyStruct>(address);

Addressing Modes

Absolute Addressing (Default)

// Read at absolute address 0x12345678
int value = magic.Memory.Read<int>(0x12345678);

Relative Addressing

Relative to module base (useful for ASRR):

// Read at module_base + 0x1000
int value = magic.Memory.Read<int>(0x1000, isRelative: true);
// Equivalent to:
int value = magic.Memory.Read<int>(magic.Memory.ImageBase + 0x1000);

Using RemotePointer

The RemotePointer indexer provides fluent relative addressing:

var basePtr = magic[moduleBase];
int offset1 = basePtr.Read<int>(0x1000);
int offset2 = basePtr.Read<int>(0x2000);

// Chained offsets
var nestedPtr = magic[basePtr.Read<IntPtr>(0x1000)];
int value = nestedPtr.Read<int>(0x50);

String I/O

Reading Strings

// Read null-terminated ANSI string
string ansi = magic.Memory.ReadString(
    address,
    Encoding.ASCII,
    maxLength: 256
);

// Read null-terminated UTF-16 string
string unicode = magic.Memory.ReadString(
    address,
    Encoding.Unicode,
    maxLength: 512
);

Implementation: Reads byte-by-byte until null terminator or maxLength.

Writing Strings

// Write null-terminated string
bool success = magic.Memory.WriteString(
    address,
    "Hello World",
    Encoding.ASCII
);

Implementation: Writes bytes + null terminator.


Array I/O

Reading Arrays

// Read 10 integers
int[] values = magic.Memory.Read<int>(address, 10);

// Read struct array
var enemies = magic.Memory.Read<EnemyStruct>(enemyListPtr, 50);

Performance:

  • Blittable: MemoryMarshal.Read<T> in a loop (fast)
  • Marshalled: Marshal.PtrToStructure<T> per element (slow)

Writing Arrays

// Write integer array
int[] values = { 1, 2, 3, 4, 5 };
magic.Memory.Write(address, values);

// Write struct array
EnemyStruct[] enemies = GetEnemies();
magic.Memory.Write(enemyListPtr, enemies);

Raw Byte I/O

Reading Bytes

// Read 100 bytes
byte[] buffer = magic.Memory.ReadBytes(address, 100);

// Read with relative addressing
byte[] code = magic.Memory.ReadBytes(offset, count, isRelative: true);

Writing Bytes

// Write byte array
byte[] patchBytes = { 0x90, 0x90, 0x90 }; // NOP x3
int written = magic.Memory.WriteBytes(address, patchBytes);

// Write with relative addressing
int written = magic.Memory.WriteBytes(
    offset,
    new byte[] { 0x01, 0x02, 0x03 },
    isRelative: true
);

Error Handling

Read Operations

Strategy: Explicit failures → throw exceptions

try
{
    int value = magic.Memory.Read<int>(address);
}
catch (Win32Exception ex)
{
    // ReadProcessMemory failed (access violation, process exited, etc.)
    Console.WriteLine($"Read failed: {ex.Message}");
}
catch (InvalidOperationException ex)
{
    // Process handle is closed
    Console.WriteLine($"Process not open: {ex.Message}");
}

Returns: default(T) if fewer bytes read than expected (e.g., partial read).

Write Operations

Strategy: Silent failures → return false

bool success = magic.Memory.Write(address, 999);
if (!success)
{
    // Handle failure (retry, log, etc.)
}

Reason: Writes to another process can legitimately fail (protection changed, process exited); silent retry is often the right strategy.

String Operations

Read: Returns string.Empty on failure.

string text = magic.Memory.ReadString(address, Encoding.ASCII);
if (string.IsNullOrEmpty(text))
{
    // Read failed or string is empty
}

Write: Returns false on failure.


Thread Safety

ExternalReader

Thread-safe: Yes

ReadProcessMemory/WriteProcessMemory are thread-safe at the OS level. No synchronization required.

InProcessReader

Thread-safe: Yes

RPM-on-self is thread-safe. No synchronization required.

High-Level Access

Thread-safe: Depends on usage

// SAFE: Concurrent reads from multiple threads
int v1 = magic.Memory.Read<int>(addr1);
int v2 = magic.Memory.Read<int>(addr2);

// SAFE: Same address, concurrent reads (no race, just inconsistent value)
int v3 = magic.Memory.Read<int>(addr);
int v4 = magic.Memory.Read<int>(addr);

// UNSAFE: Concurrent writes (last write wins, no atomicity)
magic.Memory.Write(addr, 1);  // Thread 1
magic.Memory.Write(addr, 2);  // Thread 2 (may win)

For atomic read-modify-write, use MainThreadPump or implement locking.


Performance Characteristics

Operation Cost Notes
Read<T> (blittable) Low MemoryMarshal.Read, no alloc
Read<T> (marshalled) Medium Marshal.PtrToStructure + alloc
Read<T> (array, blittable) Medium Per-element MemoryMarshal.Read
Read<T> (array, marshalled) High Per-element marshalling + alloc
ReadBytes Low Direct buffer copy
ReadString Medium Byte-by-byte + encoding + alloc
Write<T> (blittable) Low MemoryMarshal.Write + buffer alloc
Write<T> (marshalled) Medium Marshal.StructureToPtr + buffer alloc
WriteBytes Low Direct buffer copy
WriteString Medium Encoding + null terminator + buffer alloc

Optimization Tips

  1. Prefer blittable types: Use int instead of bool where possible
  2. Batch reads: Read arrays instead of individual elements
  3. Reuse buffers: For repeated reads, reuse byte arrays
  4. Cache offsets: Compute addresses once, reuse them
  5. Use MarshalCache: Automatic via Read<T>/Write<T>

Common Patterns

Pattern: Reading a Nested Structure

// Assume structure: GameManager -> PlayerList -> Player[i] -> Health
IntPtr gameManagerPtr = magic.Memory.ImageBase + 0x1000;
IntPtr playerListPtr = magic.Memory.Read<IntPtr>(gameManagerPtr + 0x20);
IntPtr playerPtr = magic.Memory.Read<IntPtr>(playerListPtr + (playerIndex * 8));
int health = magic.Memory.Read<int>(playerPtr + 0x4);

Using RemotePointer:

int health = magic[gameManagerPtr]
    .Read<IntPtr>(0x20)        // PlayerList
    .Let(ptr => magic[ptr]
        .Read<IntPtr>(playerIndex * 8))  // Player
    .Let(ptr => magic[ptr]
        .Read<int>(0x4));      // Health

Pattern: Scanning for a Value

// Scan memory region for a specific value
IntPtr found = IntPtr.Zero;
byte[] region = magic.Memory.ReadBytes(baseAddr, size);

for (int i = 0; i < region.Length - 4; i++)
{
    int value = BitConverter.ToInt32(region, i);
    if (value == targetValue)
    {
        found = baseAddr + i;
        break;
    }
}

Better: Use PatternScanner (see Discovery).

Pattern: Safe Retry Loop

// Retry write with exponential backoff
int attempts = 0;
bool success = false;
while (attempts < 5 && !success)
{
    success = magic.Memory.Write(address, value);
    if (!success)
    {
        attempts++;
        Thread.Sleep(100 * (1 << attempts)); // 100ms, 200ms, 400ms, ...
    }
}

Troubleshooting

"Read returns default value"

Possible causes:

  1. Address is invalid
  2. Process has exited
  3. Memory protection doesn't allow read
  4. Fewer bytes read than expected (partial read)

Solutions:

  • Verify address with debugger
  • Check magic.Memory.Handle.IsInvalid
  • Use CanRead helper (if available)
  • Validate ReadBytes length

"Write returns false"

Possible causes:

  1. Memory protection is read-only
  2. Process has exited
  3. Address is invalid
  4. Anti-cheat blocking writes

Solutions:

  • Verify address with debugger
  • Check memory protection (VirtualQueryEx)
  • Ensure process has PROCESS_VM_WRITE access
  • Retry after VirtualProtectEx (if you have rights)

"Performance is slow"

Possible causes:

  1. Reading individual elements in a loop
  2. Using marshalled types extensively
  3. Small reads/writes (not batching)

Solutions:

  • Read arrays instead of loops
  • Use blittable types where possible
  • Batch reads/writes
  • Cache frequently accessed values

Further Reading