# 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: ```csharp 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 bytes, bool isRelative = false); // Typed I/O public T Read(IntPtr address, bool isRelative = false) where T : struct; public bool Write(IntPtr address, T value, bool isRelative = false) where T : struct; // Array I/O public T[] Read(IntPtr address, int count, bool isRelative = false) where T : struct; public bool Write(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 ```csharp 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. ```csharp using var magic = Magic.OpenInProcess(); // Uses InProcessReader internally ``` --- ## Typed I/O with MarshalCache ### Performance Optimization `MarshalCache` eliminates per-call reflection overhead by caching type metadata once: ```csharp public static class MarshalCache { // 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` — 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: ```csharp // Example: bool field struct MyStruct { public bool Flag; // 1 byte managed, 4 bytes Win32 BOOL public int Value; } MarshalCache.Size == 5; // Managed layout MarshalCache.MarshalSize == 8; // Win32 BOOL is 4 bytes ``` ### Usage ```csharp using var magic = Magic.Open(process); // Blittable read (fast path) int health = magic.Memory.Read(address); // Marshalled read (slow path) var gameState = magic.Memory.Read(address); ``` --- ## Addressing Modes ### Absolute Addressing (Default) ```csharp // Read at absolute address 0x12345678 int value = magic.Memory.Read(0x12345678); ``` ### Relative Addressing Relative to module base (useful for ASRR): ```csharp // Read at module_base + 0x1000 int value = magic.Memory.Read(0x1000, isRelative: true); // Equivalent to: int value = magic.Memory.Read(magic.Memory.ImageBase + 0x1000); ``` ### Using RemotePointer The `RemotePointer` indexer provides fluent relative addressing: ```csharp var basePtr = magic[moduleBase]; int offset1 = basePtr.Read(0x1000); int offset2 = basePtr.Read(0x2000); // Chained offsets var nestedPtr = magic[basePtr.Read(0x1000)]; int value = nestedPtr.Read(0x50); ``` --- ## String I/O ### Reading Strings ```csharp // 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 ```csharp // Write null-terminated string bool success = magic.Memory.WriteString( address, "Hello World", Encoding.ASCII ); ``` **Implementation:** Writes bytes + null terminator. --- ## Array I/O ### Reading Arrays ```csharp // Read 10 integers int[] values = magic.Memory.Read(address, 10); // Read struct array var enemies = magic.Memory.Read(enemyListPtr, 50); ``` **Performance:** - Blittable: `MemoryMarshal.Read` in a loop (fast) - Marshalled: `Marshal.PtrToStructure` per element (slow) ### Writing Arrays ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp try { int value = magic.Memory.Read(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` ```csharp 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. ```csharp 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 ```csharp // SAFE: Concurrent reads from multiple threads int v1 = magic.Memory.Read(addr1); int v2 = magic.Memory.Read(addr2); // SAFE: Same address, concurrent reads (no race, just inconsistent value) int v3 = magic.Memory.Read(addr); int v4 = magic.Memory.Read(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` (blittable) | Low | `MemoryMarshal.Read`, no alloc | | `Read` (marshalled) | Medium | `Marshal.PtrToStructure` + alloc | | `Read` (array, blittable) | Medium | Per-element `MemoryMarshal.Read` | | `Read` (array, marshalled) | High | Per-element marshalling + alloc | | `ReadBytes` | Low | Direct buffer copy | | `ReadString` | Medium | Byte-by-byte + encoding + alloc | | `Write` (blittable) | Low | `MemoryMarshal.Write` + buffer alloc | | `Write` (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`/`Write` --- ## Common Patterns ### Pattern: Reading a Nested Structure ```csharp // Assume structure: GameManager -> PlayerList -> Player[i] -> Health IntPtr gameManagerPtr = magic.Memory.ImageBase + 0x1000; IntPtr playerListPtr = magic.Memory.Read(gameManagerPtr + 0x20); IntPtr playerPtr = magic.Memory.Read(playerListPtr + (playerIndex * 8)); int health = magic.Memory.Read(playerPtr + 0x4); ``` **Using RemotePointer:** ```csharp int health = magic[gameManagerPtr] .Read(0x20) // PlayerList .Let(ptr => magic[ptr] .Read(playerIndex * 8)) // Player .Let(ptr => magic[ptr] .Read(0x4)); // Health ``` ### Pattern: Scanning for a Value ```csharp // 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](./discovery.md)). ### Pattern: Safe Retry Loop ```csharp // 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 - [Architecture](./architecture.md) — MemoryBase layer design - [Discovery](./discovery.md) — Pattern scanning and PE parsing - [Execution Models](./execution-models.md) — Safe execution in target process