Emul8 is a Windows x64 C++20 library for executing machine code through Unicorn while routing guest memory, imports, system calls, and execution controls through replaceable providers.
It is intentionally small: one CMake target, a public API, provider interfaces, and a Unicorn backend. The host decides which process, memory view, environment data, and execution rules are visible to each invocation.
- Windows x64 execution through Unicorn and Zydis
- Static or shared library builds
- Current-process, remote-process, and custom memory providers
- Explicit PEB and TEB providers with no implicit remote discovery
- PE module and import catalogues backed by
IMemoryProvider - Recursive direct, indirect, nested, and tail-call execution
- Reusable component profiles and per-invocation overrides
- Replacement, sparse overlay, copy-on-write, and alias memory bindings
- Local-only, forwarded, and denied writes
- Instruction, call, return, memory, and syscall rules
- Conditions, register changes, memory patches, pause, resume, stop, skip, redirect, and call replacement
- External call and system call interception
- Explicit register, stack, scratch, and return-sentinel configuration
- Windows x64
- CMake 3.24 or newer
- C++20 compiler: MSVC or clang-cl
cmake -S . -B build\clang-x64 -G Ninja -DCMAKE_CXX_COMPILER=clang-cl
cmake --build build\clang-x64 --parallel
ctest --test-dir build\clang-x64 --output-on-failureBuild a shared library with the same target:
cmake -S . -B build\clang-x64-shared -G Ninja -DBUILD_SHARED_LIBS=ON -DCMAKE_CXX_COMPILER=clang-cl
cmake --build build\clang-x64-shared --parallel
ctest --test-dir build\clang-x64-shared --output-on-failureThe public target is Emul8::Emul8. The current build also produces a deterministic remote-process integration fixture used by the tests.
#include <Emul8/Api/Emul8.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <utility>
int main()
{
auto memoryProvider = std::make_shared<Emul8::Adapters::Windows::CurrentProcessMemoryProvider>();
auto profileResult = Emul8::Api::ComponentProfile::Create(memoryProvider);
if (!profileResult)
return 1;
auto memoryRange = Emul8::Api::AddressRange(0x70000000, 0x10000);
auto memoryLayout = Emul8::Api::InvocationMemoryLayout(memoryRange, std::nullopt, 0x71000000);
auto executionLimits = Emul8::Api::ExecutionLimits(100000, 0);
auto configuration = Emul8::Api::CpuConfiguration(std::move(profileResult).TakeValue(), memoryLayout, executionLimits);
auto cpuResult = Emul8::Api::CreateCpu(configuration);
if (!cpuResult)
return 1;
auto result = Emul8::Api::InvokeWin64<std::uint64_t>(
*cpuResult.Value(),
0x0000000140001000,
std::uint64_t{7});
return result && result.Value() == 42 ? 0 : 1;
}The target address and every memory range must be valid in the selected provider or an explicit profile. The stack and return sentinel are local invocation ranges; the library does not allocate hidden guest memory.
IMemoryProvider is the only required memory contract:
class MyMemoryProvider final : public Emul8::Spi::IMemoryProvider
{
public:
Emul8::Api::Result<Emul8::Api::MemoryRegion> QueryRegion(std::uint64_t address) const override;
Emul8::Api::Result<void> ReadMemory(std::uint64_t address, std::span<std::byte> destination) const override;
Emul8::Api::Result<void> WriteMemory(std::uint64_t address, std::span<const std::byte> source) override;
};Use CurrentProcessMemoryProvider for the host process. Use RemoteProcessMemoryProvider with an already opened borrowed HANDLE for another process. The provider never opens, suspends, or discovers a remote process on its own.
Environment data is a separate contract:
Emul8::Adapters::Windows::CurrentProcessWindowsEnvironmentProvider currentEnvironment;
Emul8::Adapters::Windows::RemoteProcessWindowsEnvironmentProvider remoteEnvironment(remotePeb, remoteTeb);WindowsModuleCatalog and WindowsImportCatalog consume an explicit provider and address. Remote PEB, TEB, TLS, and module metadata therefore remain under host control.
Profiles are immutable and reusable. Each root invocation receives a fresh overlay state.
Emul8::Api::MemoryProfileBuilder builder;
builder.AddReplacement(100, dataAddress, replacementBytes);
builder.AddSparseOverlay(90, Emul8::Api::AddressRange(dataAddress, 0x100), sparseBytes);
builder.AddCopyOnWrite(80, Emul8::Api::AddressRange(dataAddress, 0x1000));
builder.AddAlias(70, Emul8::Api::AddressRange(aliasAddress, 0x100), otherProvider, sourceAddress);
auto profileResult = builder.Build();Higher priority wins. Equal-priority overlapping ranges are rejected. EMirrorWriteMode::LOCAL_ONLY changes only the invocation view, EMirrorWriteMode::FORWARD writes through to the selected provider, and EMirrorWriteMode::DENY rejects the write.
Rules are attached to a component profile and inherited by descendant calls. A rule selects an event, evaluates conditions, and applies ordered actions. Actions can pause or stop execution, edit registers, patch memory, skip an instruction, redirect a call, or replace a call.
IExternalCallProvider receives a resolved call target before the guest call executes. It can let the call continue, return directly to the caller, or redirect it while preserving the Win64 call return address. ISystemCallProvider receives guest syscall requests; the core never executes a host syscall implicitly.
When a remote provider is used, Unicorn receives an internal page backing for translation, but every guest fetch, read, and write is checked against the effective MemorySpace. Data reads are refreshed from the provider. Guest writes are forwarded to the provider without creating a hidden local mirror.
Remote processes are not suspended. Reads can be torn if the source process mutates memory concurrently. Code pages freeze on first fetch; call IInvocation::InvalidateCode after an intentional remote code change.
Each invocation is single-threaded and owns its Unicorn engine, registers, stack, scratch ranges, mappings, rules, and overlay state. Parallelism is achieved with independent CPU and invocation objects.
The current backend supports Windows x64 only.