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.
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
# 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<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
|
||||
|
||||
```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<T>
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
`MarshalCache<T>` eliminates per-call reflection overhead by caching type metadata once:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
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)
|
||||
|
||||
```csharp
|
||||
// Read at absolute address 0x12345678
|
||||
int value = magic.Memory.Read<int>(0x12345678);
|
||||
```
|
||||
|
||||
### Relative Addressing
|
||||
|
||||
Relative to module base (useful for ASRR):
|
||||
|
||||
```csharp
|
||||
// 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:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```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<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
|
||||
|
||||
```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<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`
|
||||
|
||||
```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<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
|
||||
|
||||
```csharp
|
||||
// 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:**
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user