## Context BlackMagic is a pure C# process manipulation library. It replaced FASM (native x86 assembler DLL) with hand-assembled `byte[]` stubs. Two capabilities were lost: - `InjectAndExecuteEx`: non-blocking remote thread that returns a handle without waiting. - Text-based assembly: build shellcode from `pushad`, `mov eax, 0x1234`, etc. instead of raw bytes. Existing code: - `BMThread.cs` has blocking `Execute(addr, param)` → waits 10s, returns exit code. - `BMThread.cs` has `CreateRemoteThread(addr, param)` → returns `SafeMemoryHandle?`. - `SInject.cs` has `InjectCode(addr, bytes)` and `InjectCode(bytes)` → allocate + write. - `BlackMagicTest/` uses xUnit, `net8.0-windows`, pure-logic tests (no process needed). ## Goals / Non-Goals **Goals:** - Add `InjectAndExecuteEx()`: inject code + create remote thread, return handle, do NOT wait. - Add `AsmBuilder`: pure C# x86 text assembler. Convert `"pushad\nmov eax, 1\npopad"` → `byte[]`. - Add `SetPassLimit(int)` on `AsmBuilder` for label resolution iteration control. - Add convenience overloads: `InjectAndExecute(string asm)`, `InjectAndExecuteEx(string asm)`. - TDD: write tests first for every public API surface. **Non-Goals:** - Full x86 instruction set (only shellcode-common subset: mov, push, pop, call, jmp, ret, nop, pushad/popad, test, je/jne, inc, add, sub, xor, and, or, cmp, lea, nop, hlt). - x64 text assembly (keep x86 only; x64 stubs remain hand-assembled `byte[]`). - Reimplementing FASM's directive system (`org`, `use32`, macros). - Native DLL dependency. ## Decisions ### D1: AsmBuilder lives in `BlackMagic/Asm/AsmBuilder.cs` New directory `Asm/` keeps assembler code isolated. Static class, no instance state needed — each `Assemble()` call is self-contained. **Alternatives considered:** - Instance class with `AddLine()` builder pattern → rejected: adds statefulness for no benefit. Each shellcode is a fresh call. - Put in `Injection/` → rejected: assembler is generic, not injection-specific. ### D2: Two-pass assembler (labels + bytes) Pass 1: scan instructions, record label positions, emit bytes (reserving 4 bytes for near jumps). Pass 2: resolve label offsets, patch jump targets. This handles forward references (`jmp @skip` before `@skip:` label is defined) without multiple iterations. `SetPassLimit()` caps the loop for safety but 2 passes is sufficient for all shellcode patterns. **Alternatives considered:** - Single-pass → rejected: can't resolve forward jumps. - FASM-style multi-pass → overkill: shellcode doesn't need complex expression evaluation. ### D3: Instruction encoding via switch + helper methods Each instruction maps to hand-coded byte emission. Helper methods: `EmitU8()`, `EmitU32()`, `EmitModRM()`, `EmitSIB()`. Same pattern as `SInject.cs`'s existing `BuildStub()` methods. **Alternatives considered:** - Lookup table / dictionary → rejected: instruction encoding is irregular (ModRM, SIB, displacement, immediate). Switch statements are clearer. ### D4: InjectAndExecuteEx returns `SafeMemoryHandle` Matches existing `CreateRemoteThread()` return type. Caller is responsible for disposing the handle. No auto-wait, no auto-close. ### D5: TDD approach Write xUnit tests in `BlackMagicTest/AsmBuilderTests.cs` first: - Each instruction: known input text → expected `byte[]` output. - Label resolution: forward jump, backward jump, multiple labels. - Error cases: unknown instruction, missing operand, invalid register. - `SetPassLimit`: verify iteration cap is respected. Then implement `AsmBuilder` to make tests pass. ## Risks / Trade-offs - **Instruction subset**: users may need an instruction not in the initial set. Mitigation: document supported instructions, add new ones incrementally. - **Label complexity**: relative jumps are limited to ±127 bytes (near) or ±2GB (far). Shellcode rarely exceeds this, but document the limit. - **No runtime validation of shellcode**: the assembler produces bytes; it doesn't verify the result is safe to execute. This matches FASM's behavior — the assembler doesn't validate semantics.