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:
kbe
2026-07-22 22:26:07 +02:00
parent 040a51bf03
commit 6300bebe33
17 changed files with 4906 additions and 1 deletions
+481
View File
@@ -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