diff --git a/.gitignore b/.gitignore index aa04efc..0e97ee5 100644 --- a/.gitignore +++ b/.gitignore @@ -204,3 +204,8 @@ _ReSharper.Caches/ # unicorn.dll # libunicorn.so # libunicorn.dylib + +# Agent skills local tracker +CLAUDE.md +docs/agents/ +.scratch/ diff --git a/UnicornNet.Tests/ControlEngineTests.cs b/UnicornNet.Tests/ControlEngineTests.cs new file mode 100644 index 0000000..f990c9a --- /dev/null +++ b/UnicornNet.Tests/ControlEngineTests.cs @@ -0,0 +1,37 @@ +using System; +using Xunit; + +namespace UnicornNet.Tests; + +public sealed class ControlEngineTests +{ + [Fact] + public void ControlCommand_CarriesArgumentsWithPackedCommandValue() + { + ReadOnlySpan arguments = [(nint)0x1000, (nint)0x2000]; + + var command = Unicorn.ControlCommand.Create( + Unicorn.ControlType.TranslationBlockRemove, + arguments, + Unicorn.ControlIo.Write); + + Assert.Equal(Unicorn.ControlType.TranslationBlockRemove, command.Type); + Assert.Equal(Unicorn.ControlIo.Write, command.Access); + Assert.Equal(2, command.ArgumentCount); + Assert.Equal(arguments.ToArray(), command.Arguments.ToArray()); + } + + [Fact] + public void ControlEngine_ForwardsCommandAndArgumentsToNativeProxy() + { + var native = new FakeNativeProxy(); + var engine = new ControlEngine(native, () => new IntPtr(0x1234), () => { }); + var command = Unicorn.ControlCommand.Read(Unicorn.ControlType.PageSize, [(nint)0x4000]); + + engine.Control(command); + + Assert.True(native.LastControl.HasValue); + Assert.Equal(command.Value, native.LastControl.Value.Control); + Assert.Equal(command.Arguments.ToArray(), native.LastControl.Value.Arguments); + } +} diff --git a/UnicornNet.Tests/FakeHookManager.cs b/UnicornNet.Tests/FakeHookManager.cs new file mode 100644 index 0000000..b4b9f92 --- /dev/null +++ b/UnicornNet.Tests/FakeHookManager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace UnicornNet.Tests; + +internal sealed class FakeHookManager : IHookManager +{ + private readonly Dictionary _hooks = new(); + private nuint _nextHandle; + + public List RemovedHooks { get; } = []; + + public Unicorn.HookHandle AddHook(Unicorn.HookType type, Delegate callback, Unicorn.HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + + var handle = new Unicorn.HookHandle(++_nextHandle); + _hooks.Add(handle, (type, callback, state)); + return handle; + } + + public void RemoveHook(Unicorn.HookHandle handle) + { + if (_hooks.Remove(handle)) + { + RemovedHooks.Add(handle); + } + } + + public void Dispose() + { + _hooks.Clear(); + } + + public bool TrySimulateHook(Unicorn.HookHandle handle, ulong address, int size) + { + if (!_hooks.TryGetValue(handle, out var registration)) + { + return false; + } + + switch (registration.Type) + { + case Unicorn.HookType.Code when registration.Callback is Unicorn.CodeHook codeHook: + codeHook(null!, address, size, registration.State); + return true; + case Unicorn.HookType.Block when registration.Callback is Unicorn.BlockHook blockHook: + blockHook(null!, address, size, registration.State); + return true; + default: + return false; + } + } +} diff --git a/UnicornNet.Tests/FakeNativeProxy.cs b/UnicornNet.Tests/FakeNativeProxy.cs index 326f0ba..ea0029e 100644 --- a/UnicornNet.Tests/FakeNativeProxy.cs +++ b/UnicornNet.Tests/FakeNativeProxy.cs @@ -20,6 +20,16 @@ internal class FakeNativeProxy : IUnicornNativeProxy public List<(ulong Address, ulong Size, uint Permissions, IntPtr Pointer)> MemMapPtrRequests { get; } = []; + public (ulong Address, ulong Size, uint Permissions)? LastMemMap { get; private set; } + + public (ulong Address, ulong Size)? LastMemUnmap { get; private set; } + + public (ulong Address, ulong Size, uint Permissions)? LastMemProtect { get; private set; } + + public (ulong Address, byte[] Data)? LastMemWrite { get; private set; } + + public (ulong Address, int Length)? LastMemRead { get; private set; } + public (int RegisterId, byte[] Value)? LastRegisterWrite { get; private set; } public Dictionary RegisterValues { get; } = new(); @@ -42,6 +52,7 @@ public virtual int Close(IntPtr engine) public virtual int MemMap(IntPtr engine, ulong address, ulong size, uint permissions) { + LastMemMap = (address, size, permissions); return 0; } @@ -53,21 +64,25 @@ public virtual int MemMapPtr(IntPtr engine, ulong address, ulong size, uint perm public virtual int MemUnmap(IntPtr engine, ulong address, ulong size) { + LastMemUnmap = (address, size); return 0; } public virtual int MemProtect(IntPtr engine, ulong address, ulong size, uint permissions) { + LastMemProtect = (address, size, permissions); return 0; } public virtual int MemWrite(IntPtr engine, ulong address, ReadOnlySpan data) { + LastMemWrite = (address, data.ToArray()); return 0; } public virtual int MemRead(IntPtr engine, ulong address, Span buffer) { + LastMemRead = (address, buffer.Length); buffer.Clear(); return 0; } @@ -181,4 +196,4 @@ private int RegisterHook(out nuint hookId) ActiveHooks.Add(hookId); return 0; } -} \ No newline at end of file +} diff --git a/UnicornNet.Tests/HookManagerTests.cs b/UnicornNet.Tests/HookManagerTests.cs new file mode 100644 index 0000000..6759312 --- /dev/null +++ b/UnicornNet.Tests/HookManagerTests.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Xunit; + +namespace UnicornNet.Tests; + +public sealed class HookManagerTests +{ + [Fact] + public void FakeHookManager_RegistersAndRemovesHooks() + { + var manager = new FakeHookManager(); + var invocationCount = 0; + + var handle = manager.AddHook(Unicorn.HookType.Code, new Unicorn.CodeHook((_, _, _, _) => invocationCount++)); + var invokedBeforeRemoval = manager.TrySimulateHook(handle, 0x1000, 4); + + manager.RemoveHook(handle); + var invokedAfterRemoval = manager.TrySimulateHook(handle, 0x1000, 4); + + Assert.True(invokedBeforeRemoval); + Assert.False(invokedAfterRemoval); + Assert.Equal(1, invocationCount); + Assert.Contains(handle, manager.RemovedHooks); + } + + [Fact] + public void PublicHookRegistrationMethods_AreNotGeneric() + { + var genericHookMethods = typeof(Unicorn).GetMethods() + .Where(method => method.Name.StartsWith("Add", StringComparison.Ordinal) + && method.Name.EndsWith("Hook", StringComparison.Ordinal) + && method.IsGenericMethod) + .Select(method => method.Name) + .ToArray(); + + Assert.Empty(genericHookMethods); + } +} diff --git a/UnicornNet.Tests/MemoryManagerTests.cs b/UnicornNet.Tests/MemoryManagerTests.cs new file mode 100644 index 0000000..065b569 --- /dev/null +++ b/UnicornNet.Tests/MemoryManagerTests.cs @@ -0,0 +1,120 @@ +using System; +using Xunit; + +namespace UnicornNet.Tests; + +public sealed class MemoryManagerTests +{ + private const ulong BaseAddress = 0x10000; + private const ulong RegionSize = 0x1000; + + [Fact] + public void MemoryRegion_UsesMemoryManagerForReadWriteProtectAndDispose() + { + var memory = new FakeMemoryManager(); + var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); + var data = new byte[] { 1, 2, 3 }; + Span buffer = stackalloc byte[3]; + + region.Write(data, 4); + region.Read(buffer, 4); + region.Protect(Unicorn.MemoryPermissions.Read); + region.Dispose(); + + Assert.Equal((BaseAddress + 4, data), memory.LastWrite); + Assert.Equal((BaseAddress + 4, 3), memory.LastRead); + Assert.Equal((BaseAddress, RegionSize, Unicorn.MemoryPermissions.Read), memory.LastProtect); + Assert.Equal((BaseAddress, RegionSize), memory.LastUnmap); + } + + [Fact] + public void MemoryRegion_RejectsWritesOutsideRegionBounds() + { + var memory = new FakeMemoryManager(); + var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); + var data = new byte[2]; + + Assert.Throws(() => region.Write(data, RegionSize)); + } + + [Fact] + public void MemoryRegion_RejectsReadsOutsideRegionBounds() + { + var memory = new FakeMemoryManager(); + var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); + var buffer = new byte[2]; + + Assert.Throws(() => region.Read(buffer, RegionSize)); + } + + [Fact] + public void MemoryManager_MapCreatesRegionAndForwardsToNativeProxy() + { + var native = new FakeNativeProxy(); + var manager = new MemoryManager(native, () => new IntPtr(0x1234), () => { }); + + var region = manager.Map(BaseAddress, RegionSize, Unicorn.MemoryPermissions.Read); + + Assert.Equal(BaseAddress, region.Address); + Assert.Equal(RegionSize, region.Size); + Assert.Equal(Unicorn.MemoryPermissions.Read, region.Permissions); + Assert.Equal((BaseAddress, RegionSize, (uint)Unicorn.MemoryPermissions.Read), native.LastMemMap); + } + + [Fact] + public void MemoryManager_ReadAndWriteForwardToNativeProxy() + { + var native = new FakeNativeProxy(); + var manager = new MemoryManager(native, () => new IntPtr(0x1234), () => { }); + var data = new byte[] { 0xAA, 0xBB }; + Span buffer = stackalloc byte[2]; + + manager.Write(BaseAddress, data); + manager.Read(BaseAddress + 8, buffer); + + Assert.True(native.LastMemWrite.HasValue); + Assert.Equal(BaseAddress, native.LastMemWrite.Value.Address); + Assert.Equal(data, native.LastMemWrite.Value.Data); + Assert.Equal((BaseAddress + 8, 2), native.LastMemRead); + } +} + +internal sealed class FakeMemoryManager : IMemoryManager +{ + public (ulong Address, ulong Size, Unicorn.MemoryPermissions Permissions)? LastMap { get; private set; } + + public (ulong Address, ulong Size)? LastUnmap { get; private set; } + + public (ulong Address, ulong Size, Unicorn.MemoryPermissions Permissions)? LastProtect { get; private set; } + + public (ulong Address, byte[] Data)? LastWrite { get; private set; } + + public (ulong Address, int Length)? LastRead { get; private set; } + + public MemoryRegion Map(ulong address, ulong size, Unicorn.MemoryPermissions permissions) + { + LastMap = (address, size, permissions); + return new MemoryRegion(this, address, size, permissions); + } + + public void Unmap(ulong address, ulong size) + { + LastUnmap = (address, size); + } + + public void Protect(ulong address, ulong size, Unicorn.MemoryPermissions permissions) + { + LastProtect = (address, size, permissions); + } + + public void Write(ulong address, ReadOnlySpan data) + { + LastWrite = (address, data.ToArray()); + } + + public void Read(ulong address, Span buffer) + { + LastRead = (address, buffer.Length); + buffer.Clear(); + } +} diff --git a/UnicornNet.Tests/NewFeaturesTests.cs b/UnicornNet.Tests/NewFeaturesTests.cs index 35a4122..e050153 100644 --- a/UnicornNet.Tests/NewFeaturesTests.cs +++ b/UnicornNet.Tests/NewFeaturesTests.cs @@ -13,7 +13,8 @@ public sealed class NewFeaturesTests [Fact] public void MemoryRegion_TracksAddressAndSize() { - var region = new MemoryRegion(null!, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); + var memory = new FakeMemoryManager(); + var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); var expectedEndAddress = BaseAddress + RegionSize; Assert.Equal(BaseAddress, region.Address); @@ -25,7 +26,8 @@ public void MemoryRegion_TracksAddressAndSize() [Fact] public void MemoryRegion_ContainsCheck_ReturnsCorrectResult() { - var region = new MemoryRegion(null!, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); + var memory = new FakeMemoryManager(); + var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); var middleAddress = BaseAddress + 0x500; var lastAddress = BaseAddress + RegionSize - 1; var beforeStart = BaseAddress - 1; @@ -41,7 +43,8 @@ public void MemoryRegion_ContainsCheck_ReturnsCorrectResult() [Fact] public void MemoryRegion_ContainsRange_ReturnsCorrectResult() { - var region = new MemoryRegion(null!, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); + var memory = new FakeMemoryManager(); + var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All); const ulong smallSize = 0x100; const ulong exceedsRegionByOne = RegionSize + 1; const ulong largeSize = 0x200; @@ -302,4 +305,4 @@ public override int MemMapPtr(IntPtr engine, ulong address, ulong size, uint per return (int)Unicorn.ErrorCode.Map; } } -} \ No newline at end of file +} diff --git a/UnicornNet.Tests/RegisterBankTests.cs b/UnicornNet.Tests/RegisterBankTests.cs new file mode 100644 index 0000000..f1ff34e --- /dev/null +++ b/UnicornNet.Tests/RegisterBankTests.cs @@ -0,0 +1,48 @@ +using System; +using Xunit; + +namespace UnicornNet.Tests; + +public sealed class RegisterBankTests +{ + [Fact] + public void RegisterBank_WriteSerializesValueToNativeProxy() + { + var native = new FakeNativeProxy(); + var bank = new RegisterBank(native, () => new IntPtr(0x1234), () => { }); + const ulong value = 0x1122334455667788UL; + + bank.Write((int)Unicorn.Registers.X86.RAX, value); + + Assert.True(native.LastRegisterWrite.HasValue); + Assert.Equal((int)Unicorn.Registers.X86.RAX, native.LastRegisterWrite.Value.RegisterId); + Assert.Equal(BitConverter.GetBytes(value), native.LastRegisterWrite.Value.Value); + } + + [Fact] + public void RegisterBank_ReadDeserializesValueFromNativeProxy() + { + var native = new FakeNativeProxy(); + const int registerId = (int)Unicorn.Registers.Arm.R0; + const uint expected = 0xAABBCCDD; + native.RegisterValues[registerId] = BitConverter.GetBytes(expected); + var bank = new RegisterBank(native, () => new IntPtr(0x1234), () => { }); + + var actual = bank.Read(registerId); + + Assert.Equal(expected, actual); + } + + [Fact] + public void RegisterBank_NormalizesMultipleArchitectureRegisterEnums() + { + var native = new FakeNativeProxy(); + var bank = new RegisterBank(native, () => new IntPtr(0x1234), () => { }); + + bank.Write(Unicorn.Registers.X86.RBX, 1UL); + Assert.Equal((int)Unicorn.Registers.X86.RBX, native.LastRegisterWrite!.Value.RegisterId); + + bank.Write(Unicorn.Registers.Arm.R1, 2U); + Assert.Equal((int)Unicorn.Registers.Arm.R1, native.LastRegisterWrite!.Value.RegisterId); + } +} diff --git a/UnicornNet/Control/ControlEngine.cs b/UnicornNet/Control/ControlEngine.cs new file mode 100644 index 0000000..907035a --- /dev/null +++ b/UnicornNet/Control/ControlEngine.cs @@ -0,0 +1,38 @@ +using System; + +namespace UnicornNet; + +internal sealed class ControlEngine : IControlEngine +{ + private readonly Action _ensureNotDisposed; + private readonly Func _getEngineHandle; + private readonly IUnicornNativeProxy _native; + + public ControlEngine(IUnicornNativeProxy native, Func getEngineHandle, Action ensureNotDisposed) + { + ArgumentNullException.ThrowIfNull(native); + ArgumentNullException.ThrowIfNull(getEngineHandle); + ArgumentNullException.ThrowIfNull(ensureNotDisposed); + + _native = native; + _getEngineHandle = getEngineHandle; + _ensureNotDisposed = ensureNotDisposed; + } + + public void Control(Unicorn.ControlCommand command) + { + _ensureNotDisposed(); + + if (command.Arguments.Length != command.ArgumentCount) + { + throw new ArgumentException("Command arguments length must match the command argument count.", nameof(command)); + } + + var err = _native.Control(_getEngineHandle(), command.Value, command.Arguments); + if (err != 0) + { + throw new UnicornEngineException((Unicorn.ErrorCode)err, "uc_ctl"); + } + } + } +} diff --git a/UnicornNet/Control/IControlEngine.cs b/UnicornNet/Control/IControlEngine.cs new file mode 100644 index 0000000..ef7754f --- /dev/null +++ b/UnicornNet/Control/IControlEngine.cs @@ -0,0 +1,6 @@ +namespace UnicornNet; + +public interface IControlEngine +{ + void Control(Unicorn.ControlCommand command); +} diff --git a/UnicornNet/Control/Unicorn.Control.cs b/UnicornNet/Control/Unicorn.Control.cs new file mode 100644 index 0000000..fde1896 --- /dev/null +++ b/UnicornNet/Control/Unicorn.Control.cs @@ -0,0 +1,161 @@ +using System; + +namespace UnicornNet; + +public partial class Unicorn +{ + public void Control(ControlCommand command) + { + _control.Control(command); + } + + internal void Control(ControlCommand command, nint arg1) + { + Control(command.WithArguments([arg1])); + } + + internal void Control(ControlCommand command, nint arg1, nint arg2) + { + Control(command.WithArguments([arg1, arg2])); + } + + internal void Control(ControlCommand command, nint arg1, nint arg2, nint arg3) + { + Control(command.WithArguments([arg1, arg2, arg3])); + } + + internal void Control(ControlCommand command, nint arg1, nint arg2, nint arg3, nint arg4) + { + Control(command.WithArguments([arg1, arg2, arg3, arg4])); + } + + internal void Control(ControlCommand command, ReadOnlySpan arguments) + { + Control(command.WithArguments(arguments)); + } + + internal void Control(ControlType type, ControlIo access) + { + Control(ControlCommand.Create(type, 0, access)); + } + + internal void Control(ControlType type, ControlIo access, nint arg1) + { + Control(ControlCommand.Create(type, [arg1], access)); + } + + internal void Control(ControlType type, ControlIo access, nint arg1, nint arg2) + { + Control(ControlCommand.Create(type, [arg1, arg2], access)); + } + + internal void Control(ControlType type, ControlIo access, nint arg1, nint arg2, nint arg3) + { + Control(ControlCommand.Create(type, [arg1, arg2, arg3], access)); + } + + internal void Control(ControlType type, ControlIo access, nint arg1, nint arg2, nint arg3, nint arg4) + { + Control(ControlCommand.Create(type, [arg1, arg2, arg3, arg4], access)); + } + + internal void Control(ControlType type, ControlIo access, ReadOnlySpan arguments) + { + Control(ControlCommand.Create(type, arguments, access)); + } + + internal void ControlRead(ControlType type) + { + Control(type, ControlIo.Read); + } + + internal void ControlRead(ControlType type, nint arg1) + { + Control(type, ControlIo.Read, arg1); + } + + internal void ControlRead(ControlType type, nint arg1, nint arg2) + { + Control(type, ControlIo.Read, arg1, arg2); + } + + internal void ControlRead(ControlType type, nint arg1, nint arg2, nint arg3) + { + Control(type, ControlIo.Read, arg1, arg2, arg3); + } + + internal void ControlRead(ControlType type, nint arg1, nint arg2, nint arg3, nint arg4) + { + Control(type, ControlIo.Read, arg1, arg2, arg3, arg4); + } + + internal void ControlRead(ControlType type, ReadOnlySpan arguments) + { + Control(type, ControlIo.Read, arguments); + } + + internal void ControlWrite(ControlType type) + { + Control(type, ControlIo.Write); + } + + internal void ControlWrite(ControlType type, nint arg1) + { + Control(type, ControlIo.Write, arg1); + } + + internal void ControlWrite(ControlType type, nint arg1, nint arg2) + { + Control(type, ControlIo.Write, arg1, arg2); + } + + internal void ControlWrite(ControlType type, nint arg1, nint arg2, nint arg3) + { + Control(type, ControlIo.Write, arg1, arg2, arg3); + } + + internal void ControlWrite(ControlType type, nint arg1, nint arg2, nint arg3, nint arg4) + { + Control(type, ControlIo.Write, arg1, arg2, arg3, arg4); + } + + internal void ControlWrite(ControlType type, ReadOnlySpan arguments) + { + Control(type, ControlIo.Write, arguments); + } + + internal void ControlReadWrite(ControlType type) + { + Control(type, ControlIo.ReadWrite); + } + + internal void ControlReadWrite(ControlType type, nint arg1) + { + Control(type, ControlIo.ReadWrite, arg1); + } + + internal void ControlReadWrite(ControlType type, nint arg1, nint arg2) + { + Control(type, ControlIo.ReadWrite, arg1, arg2); + } + + internal void ControlReadWrite(ControlType type, nint arg1, nint arg2, nint arg3) + { + Control(type, ControlIo.ReadWrite, arg1, arg2, arg3); + } + + internal void ControlReadWrite(ControlType type, nint arg1, nint arg2, nint arg3, nint arg4) + { + Control(type, ControlIo.ReadWrite, arg1, arg2, arg3, arg4); + } + + internal void ControlReadWrite(ControlType type, ReadOnlySpan arguments) + { + Control(type, ControlIo.ReadWrite, arguments); + } + + internal void ControlNone(ControlType type) + { + Control(type, ControlIo.None); + } +} diff --git a/UnicornNet/HookBuilder.cs b/UnicornNet/Hooks/HookBuilder.cs similarity index 79% rename from UnicornNet/HookBuilder.cs rename to UnicornNet/Hooks/HookBuilder.cs index 0294071..10e6d6a 100644 --- a/UnicornNet/HookBuilder.cs +++ b/UnicornNet/Hooks/HookBuilder.cs @@ -8,12 +8,12 @@ namespace UnicornNet; /// public sealed class HookBuilder { - private readonly Unicorn _engine; + private readonly IHookManager _hooks; private readonly List _handles = []; - internal HookBuilder(Unicorn engine) + internal HookBuilder(IHookManager hooks) { - _engine = engine; + _hooks = hooks; } /// @@ -22,7 +22,7 @@ internal HookBuilder(Unicorn engine) public HookBuilder OnCode(Unicorn.CodeHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddCodeHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.Code, callback, range, state); _handles.Add(handle); return this; } @@ -33,7 +33,7 @@ public HookBuilder OnCode(Unicorn.CodeHook callback, Unicorn.HookRange? range = public HookBuilder OnBlock(Unicorn.BlockHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddBlockHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.Block, callback, range, state); _handles.Add(handle); return this; } @@ -44,7 +44,7 @@ public HookBuilder OnBlock(Unicorn.BlockHook callback, Unicorn.HookRange? range public HookBuilder OnMemoryRead(Unicorn.MemoryHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddMemReadHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.MemRead, callback, range, state); _handles.Add(handle); return this; } @@ -55,7 +55,7 @@ public HookBuilder OnMemoryRead(Unicorn.MemoryHook callback, Unicorn.HookRange? public HookBuilder OnMemoryWrite(Unicorn.MemoryHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddMemWriteHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.MemWrite, callback, range, state); _handles.Add(handle); return this; } @@ -66,7 +66,7 @@ public HookBuilder OnMemoryWrite(Unicorn.MemoryHook callback, Unicorn.HookRange? public HookBuilder OnInterrupt(Unicorn.InterruptHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddInterruptHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.Interrupt, callback, range, state); _handles.Add(handle); return this; } @@ -77,7 +77,7 @@ public HookBuilder OnInterrupt(Unicorn.InterruptHook callback, Unicorn.HookRange public HookBuilder OnIn(Unicorn.InHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddInHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.Instruction, callback, range, state); _handles.Add(handle); return this; } @@ -88,7 +88,7 @@ public HookBuilder OnIn(Unicorn.InHook callback, Unicorn.HookRange? range = null public HookBuilder OnOut(Unicorn.OutHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddOutHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.Instruction, callback, range, state); _handles.Add(handle); return this; } @@ -99,7 +99,7 @@ public HookBuilder OnOut(Unicorn.OutHook callback, Unicorn.HookRange? range = nu public HookBuilder OnSyscall(Unicorn.SyscallHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddSyscallHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.Instruction, callback, range, state); _handles.Add(handle); return this; } @@ -110,7 +110,7 @@ public HookBuilder OnSyscall(Unicorn.SyscallHook callback, Unicorn.HookRange? ra public HookBuilder OnInvalidInstruction(Unicorn.InvalidInstructionHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddInvalidInstructionHook(callback, range, state); + var handle = _hooks.AddHook(Unicorn.HookType.InvalidInstruction, callback, range, state); _handles.Add(handle); return this; } @@ -121,7 +121,7 @@ public HookBuilder OnInvalidInstruction(Unicorn.InvalidInstructionHook callback, public HookBuilder OnEventMem(Unicorn.HookType eventTypes, Unicorn.MemoryEventHook callback, Unicorn.HookRange? range = null, object? state = null) { ArgumentNullException.ThrowIfNull(callback); - var handle = _engine.AddEventMemHook(eventTypes, callback, range, state); + var handle = _hooks.AddHook(eventTypes, callback, range, state); _handles.Add(handle); return this; } @@ -133,4 +133,4 @@ public HookBuilder OnEventMem(Unicorn.HookType eventTypes, Unicorn.MemoryEventHo { return _handles.ToArray(); } -} \ No newline at end of file +} diff --git a/UnicornNet/Hooks/HookManager.cs b/UnicornNet/Hooks/HookManager.cs new file mode 100644 index 0000000..de010e9 --- /dev/null +++ b/UnicornNet/Hooks/HookManager.cs @@ -0,0 +1,832 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace UnicornNet; + +internal sealed class HookManager : IHookManager +{ + private const Unicorn.HookType EventMemoryHookMask = Unicorn.HookType.MemReadUnmapped + | Unicorn.HookType.MemWriteUnmapped + | Unicorn.HookType.MemFetchUnmapped + | Unicorn.HookType.MemReadProt + | Unicorn.HookType.MemWriteProt + | Unicorn.HookType.MemFetchProt; + + private const int X86InstructionIn = 218; + private const int X86InstructionOut = 500; + private const int X86InstructionSyscall = 699; + + private static readonly NativeHookCallback HookThunk = OnNativeHook; + private static readonly NativeMemHookCallback MemHookThunk = OnNativeMemHook; + private static readonly NativeEventMemHookCallback EventMemHookThunk = OnNativeEventMemHook; + private static readonly NativeInterruptHookCallback InterruptHookThunk = OnNativeInterruptHook; + private static readonly NativeInstructionInHookCallback InstructionInHookThunk = OnNativeInstructionInHook; + private static readonly NativeInstructionOutHookCallback InstructionOutHookThunk = OnNativeInstructionOutHook; + private static readonly NativeSyscallHookCallback SyscallHookThunk = OnNativeSyscallHook; + private static readonly NativeInvalidInstructionHookCallback InvalidInstructionHookThunk = OnNativeInvalidInstructionHook; + + private readonly ConcurrentDictionary> _eventMemRegistrations = new(); + private readonly Func _getEngineHandle; + private readonly ConcurrentDictionary _hookRegistry = new(); + private readonly IUnicornNativeProxy _native; + private readonly Unicorn _owner; + private readonly Action _ensureNotDisposed; + + public HookManager(Unicorn owner, IUnicornNativeProxy native, Func getEngineHandle, Action ensureNotDisposed) + { + ArgumentNullException.ThrowIfNull(owner); + ArgumentNullException.ThrowIfNull(native); + ArgumentNullException.ThrowIfNull(getEngineHandle); + ArgumentNullException.ThrowIfNull(ensureNotDisposed); + + _owner = owner; + _native = native; + _getEngineHandle = getEngineHandle; + _ensureNotDisposed = ensureNotDisposed; + } + + public Unicorn.HookHandle AddHook(Unicorn.HookType type, Delegate callback, Unicorn.HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + + return type switch + { + Unicorn.HookType.Code when callback is Unicorn.CodeHook => RegisterCodeOrBlockHook(type, callback, state, range), + Unicorn.HookType.Block when callback is Unicorn.BlockHook => RegisterCodeOrBlockHook(type, callback, state, range), + Unicorn.HookType.MemRead when callback is Unicorn.MemoryHook memoryHook => RegisterMemoryHook(type, Unicorn.MemoryAccessType.Read, memoryHook, range, state), + Unicorn.HookType.MemWrite when callback is Unicorn.MemoryHook memoryHook => RegisterMemoryHook(type, Unicorn.MemoryAccessType.Write, memoryHook, range, state), + Unicorn.HookType.Interrupt when callback is Unicorn.InterruptHook interruptHook => RegisterInterruptHook(interruptHook, range, state), + Unicorn.HookType.Instruction when callback is Unicorn.InHook inHook => RegisterInHook(inHook, range, state), + Unicorn.HookType.Instruction when callback is Unicorn.OutHook outHook => RegisterOutHook(outHook, range, state), + Unicorn.HookType.Instruction when callback is Unicorn.SyscallHook syscallHook => RegisterSyscallHook(syscallHook, range, state), + Unicorn.HookType.InvalidInstruction when callback is Unicorn.InvalidInstructionHook invalidInstructionHook => RegisterInvalidInstructionHook(invalidInstructionHook, range, state), + _ when callback is Unicorn.MemoryEventHook eventHook => RegisterEventMemHook(type, eventHook, range, state), + _ => throw new ArgumentOutOfRangeException(nameof(type), "Hook type and callback delegate are not a supported combination.") + }; + } + + public void RemoveHook(Unicorn.HookHandle handle) + { + _ensureNotDisposed(); + RemoveHookInternal(handle); + } + + public void Dispose() + { + var engine = _getEngineHandle(); + + foreach (var registration in _hookRegistry.Values) + { + if (engine != IntPtr.Zero && !registration.Handle.IsEmpty) + { + _native.HookDel(engine, registration.Handle.Value); + } + + registration.Dispose(); + } + + _hookRegistry.Clear(); + _eventMemRegistrations.Clear(); + } + + internal (bool IsHealthy, int DisposedCount, int TotalCount) Validate() + { + var totalCount = _hookRegistry.Count; + var disposedCount = 0; + + foreach (var registration in _hookRegistry.Values) + { + if (registration.IsDisposed || registration.Handle.IsEmpty) + { + disposedCount++; + } + } + + return (disposedCount == 0, disposedCount, totalCount); + } + + internal bool TrySimulateHook(Unicorn.HookHandle handle, ulong address, int size) + { + if (!TryGetRegistration(handle, out var registration)) + { + return false; + } + + if (registration.Category is not (HookCategory.Code or HookCategory.Block)) + { + return false; + } + + registration.InvokeCode(address, size); + return true; + } + + internal bool TrySimulateMemoryHook(Unicorn.HookHandle handle, Unicorn.MemoryAccessType accessType, ulong address, int size, long value) + { + if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Memory) + { + return false; + } + + registration.InvokeMemory(accessType, address, size, value); + return true; + } + + internal bool TrySimulateEventMem(Unicorn.MemoryAccessType accessType, ulong address, int size, long value) + { + if (!_eventMemRegistrations.TryGetValue(accessType, out var handles) || handles.IsEmpty) + { + return false; + } + + var invoked = false; + foreach (var handleEntry in handles) + { + if (!_hookRegistry.TryGetValue(handleEntry.Key, out var registration)) + { + continue; + } + + invoked = true; + if (!registration.InvokeEventMemory(accessType, address, size, value)) + { + break; + } + } + + return invoked; + } + + internal bool TrySimulateInterruptHook(Unicorn.HookHandle handle, uint interruptNumber) + { + if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Interrupt) + { + return false; + } + + registration.InvokeInterrupt(interruptNumber); + return true; + } + + internal bool TrySimulateInHook(Unicorn.HookHandle handle, uint port, int size, out uint value) + { + if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.In) + { + value = 0; + return false; + } + + value = registration.InvokeIn(port, size); + return true; + } + + internal bool TrySimulateOutHook(Unicorn.HookHandle handle, uint port, int size, uint value) + { + if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Out) + { + return false; + } + + registration.InvokeOut(port, size, value); + return true; + } + + internal bool TrySimulateSyscallHook(Unicorn.HookHandle handle) + { + if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Syscall) + { + return false; + } + + registration.InvokeSyscall(); + return true; + } + + internal bool TrySimulateInvalidInstructionHook(Unicorn.HookHandle handle, out bool continueExecution) + { + continueExecution = false; + if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.InvalidInstruction) + { + return false; + } + + continueExecution = registration.InvokeInvalidInstruction(); + return true; + } + + private static void OnNativeHook(IntPtr engine, ulong address, uint size, IntPtr userData) + { + if (TryResolveRegistration(userData, out var registration)) + { + registration.InvokeCode(address, (int)size); + } + } + + private static void OnNativeMemHook(IntPtr engine, uint accessType, ulong address, int size, long value, IntPtr userData) + { + if (TryResolveRegistration(userData, out var registration)) + { + registration.InvokeMemory((Unicorn.MemoryAccessType)accessType, address, size, value); + } + } + + private static bool OnNativeEventMemHook(IntPtr engine, uint accessType, ulong address, int size, long value, IntPtr userData) + { + return TryResolveRegistration(userData, out var registration) && registration.InvokeEventMemory((Unicorn.MemoryAccessType)accessType, address, size, value); + } + + private static void OnNativeInterruptHook(IntPtr engine, uint interruptNumber, IntPtr userData) + { + if (TryResolveRegistration(userData, out var registration)) + { + registration.InvokeInterrupt(interruptNumber); + } + } + + private static uint OnNativeInstructionInHook(IntPtr engine, uint port, int size, IntPtr userData) + { + return TryResolveRegistration(userData, out var registration) ? registration.InvokeIn(port, size) : 0; + } + + private static void OnNativeInstructionOutHook(IntPtr engine, uint port, int size, uint value, IntPtr userData) + { + if (TryResolveRegistration(userData, out var registration)) + { + registration.InvokeOut(port, size, value); + } + } + + private static void OnNativeSyscallHook(IntPtr engine, IntPtr userData) + { + if (TryResolveRegistration(userData, out var registration)) + { + registration.InvokeSyscall(); + } + } + + private static bool OnNativeInvalidInstructionHook(IntPtr engine, IntPtr userData) + { + return TryResolveRegistration(userData, out var registration) && registration.InvokeInvalidInstruction(); + } + + private static bool TryResolveRegistration(IntPtr userData, [NotNullWhen(true)] out HookRegistration? registration) + { + registration = null; + if (userData == IntPtr.Zero) + { + return false; + } + + var gcHandle = GCHandle.FromIntPtr(userData); + registration = gcHandle.Target as HookRegistration; + return registration is not null; + } + + private Unicorn.HookHandle RegisterCodeOrBlockHook(Unicorn.HookType type, Delegate callback, object? state, Unicorn.HookRange? range) + { + var normalizedRange = NormalizeRange(range); + var category = type switch + { + Unicorn.HookType.Code => HookCategory.Code, + Unicorn.HookType.Block => HookCategory.Block, + _ => throw new ArgumentOutOfRangeException(nameof(type)) + }; + + var registration = new HookRegistration(_owner, type, category, callback, state); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAdd(engine, type, HookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterMemoryHook(Unicorn.HookType hookType, Unicorn.MemoryAccessType accessType, Unicorn.MemoryHook callback, Unicorn.HookRange? range, object? state) + { + var normalizedRange = NormalizeRange(range); + var registration = new HookRegistration(_owner, hookType, HookCategory.Memory, callback, state, accessType); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAddMem(engine, hookType, MemHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterEventMemHook(Unicorn.HookType eventTypes, Unicorn.MemoryEventHook callback, Unicorn.HookRange? range, object? state) + { + var normalizedHookTypes = NormalizeEventHookTypes(eventTypes); + var normalizedRange = NormalizeRange(range); + var registration = new HookRegistration(_owner, normalizedHookTypes, HookCategory.EventMemory, callback, state); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAddEventMem(engine, normalizedHookTypes, EventMemHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterInterruptHook(Unicorn.InterruptHook callback, Unicorn.HookRange? range, object? state) + { + var normalizedRange = NormalizeRange(range); + var registration = new HookRegistration(_owner, Unicorn.HookType.Interrupt, HookCategory.Interrupt, callback, state); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAddInterrupt(engine, Unicorn.HookType.Interrupt, InterruptHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterInHook(Unicorn.InHook callback, Unicorn.HookRange? range, object? state) + { + var normalizedRange = NormalizeRange(range); + var registration = new HookRegistration(_owner, Unicorn.HookType.Instruction, HookCategory.In, callback, state); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAddInstructionIn(engine, Unicorn.HookType.Instruction, InstructionInHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, X86InstructionIn, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterOutHook(Unicorn.OutHook callback, Unicorn.HookRange? range, object? state) + { + var normalizedRange = NormalizeRange(range); + var registration = new HookRegistration(_owner, Unicorn.HookType.Instruction, HookCategory.Out, callback, state); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAddInstructionOut(engine, Unicorn.HookType.Instruction, InstructionOutHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, X86InstructionOut, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterSyscallHook(Unicorn.SyscallHook callback, Unicorn.HookRange? range, object? state) + { + var normalizedRange = NormalizeRange(range); + var registration = new HookRegistration(_owner, Unicorn.HookType.Instruction, HookCategory.Syscall, callback, state); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAddInstructionSyscall(engine, Unicorn.HookType.Instruction, SyscallHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, X86InstructionSyscall, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterInvalidInstructionHook(Unicorn.InvalidInstructionHook callback, Unicorn.HookRange? range, object? state) + { + var normalizedRange = NormalizeRange(range); + var registration = new HookRegistration(_owner, Unicorn.HookType.InvalidInstruction, HookCategory.InvalidInstruction, callback, state); + return RegisterHookInternal(registration, normalizedRange, (engine, hookRange) => + { + var err = _native.HookAddInvalidInstruction(engine, Unicorn.HookType.InvalidInstruction, InvalidInstructionHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); + return (err, hookId); + }); + } + + private Unicorn.HookHandle RegisterHookInternal(HookRegistration registration, Unicorn.HookRange range, Func registrar) + { + _ensureNotDisposed(); + + var engine = _getEngineHandle(); + var (error, handle) = registrar(engine, range); + if (error != 0) + { + registration.Dispose(); + throw new UnicornHookException((Unicorn.ErrorCode)error, "uc_hook_add"); + } + + registration.SetHandle(new Unicorn.HookHandle(handle)); + AddRegistration(registration); + return registration.Handle; + } + + private static Unicorn.HookRange NormalizeRange(Unicorn.HookRange? range) + { + return range ?? Unicorn.HookRange.All; + } + + private bool TryGetRegistration(Unicorn.HookHandle handle, [NotNullWhen(true)] out HookRegistration? registration) + { + if (!handle.IsEmpty) + { + return _hookRegistry.TryGetValue(handle.Value, out registration); + } + + registration = null; + return false; + } + + private void AddRegistration(HookRegistration registration) + { + _hookRegistry.TryAdd(registration.Handle.Value, registration); + + if (registration.Category != HookCategory.EventMemory) + { + return; + } + + foreach (var accessType in GetEventAccessTypes(registration)) + { + var handles = _eventMemRegistrations.GetOrAdd(accessType, _ => new ConcurrentDictionary()); + handles.TryAdd(registration.Handle.Value, 0); + } + } + + private void RemoveEventRegistration(HookRegistration registration) + { + if (registration.Category != HookCategory.EventMemory) + { + return; + } + + foreach (var accessType in GetEventAccessTypes(registration)) + { + if (_eventMemRegistrations.TryGetValue(accessType, out var handles)) + { + handles.TryRemove(registration.Handle.Value, out _); + } + } + } + + private static IEnumerable GetEventAccessTypes(HookRegistration registration) + { + if (registration.AccessType.HasValue) + { + yield return registration.AccessType.Value; + yield break; + } + + if ((registration.Type & Unicorn.HookType.MemReadUnmapped) != 0) + { + yield return Unicorn.MemoryAccessType.ReadUnmapped; + } + + if ((registration.Type & Unicorn.HookType.MemWriteUnmapped) != 0) + { + yield return Unicorn.MemoryAccessType.WriteUnmapped; + } + + if ((registration.Type & Unicorn.HookType.MemFetchUnmapped) != 0) + { + yield return Unicorn.MemoryAccessType.FetchUnmapped; + } + + if ((registration.Type & Unicorn.HookType.MemReadProt) != 0) + { + yield return Unicorn.MemoryAccessType.ReadProtected; + } + + if ((registration.Type & Unicorn.HookType.MemWriteProt) != 0) + { + yield return Unicorn.MemoryAccessType.WriteProtected; + } + + if ((registration.Type & Unicorn.HookType.MemFetchProt) != 0) + { + yield return Unicorn.MemoryAccessType.FetchProtected; + } + } + + private static Unicorn.HookType NormalizeEventHookTypes(Unicorn.HookType eventTypes) + { + var normalized = eventTypes & EventMemoryHookMask; + if (normalized == 0 || (eventTypes & ~EventMemoryHookMask) != 0) + { + throw new ArgumentOutOfRangeException(nameof(eventTypes), "Event types must consist solely of UC_HOOK_MEM_* flags."); + } + + return normalized; + } + + private void RemoveHookInternal(Unicorn.HookHandle handle) + { + if (handle.IsEmpty) + { + return; + } + + if (!_hookRegistry.TryRemove(handle.Value, out var registration)) + { + return; + } + + RemoveEventRegistration(registration); + + var engine = _getEngineHandle(); + if (engine != IntPtr.Zero) + { + var err = _native.HookDel(engine, registration.Handle.Value); + if (err != 0) + { + registration.Dispose(); + throw new UnicornHookException((Unicorn.ErrorCode)err, "uc_hook_del"); + } + } + + registration.Dispose(); + } + + private enum HookCategory + { + Code, + Block, + Memory, + EventMemory, + Interrupt, + In, + Out, + Syscall, + InvalidInstruction + } + + private sealed class HookRegistration : IDisposable + { + private readonly Delegate _callback; + private readonly Unicorn _owner; + private readonly object? _state; + private GCHandle _gcHandle; + + public HookRegistration(Unicorn owner, Unicorn.HookType type, HookCategory category, Delegate callback, object? state, Unicorn.MemoryAccessType? accessType = null) + { + ArgumentNullException.ThrowIfNull(callback); + _owner = owner; + Type = type; + Category = category; + _callback = callback; + _state = state; + AccessType = accessType; + _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); + } + + public Unicorn.HookHandle Handle { get; private set; } + + public HookCategory Category { get; } + + public Unicorn.HookType Type { get; } + + public Unicorn.MemoryAccessType? AccessType { get; } + + public bool IsDisposed { get; private set; } + + public IntPtr UserDataPointer + { + get => GCHandle.ToIntPtr(_gcHandle); + } + + public void Dispose() + { + if (IsDisposed) + { + return; + } + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + + IsDisposed = true; + } + + public void SetHandle(Unicorn.HookHandle handle) + { + Handle = handle; + } + + public void InvokeCode(ulong address, int size) + { + if (IsDisposed) + { + LogDisposedHook("Code/Block"); + return; + } + + try + { + switch (Category) + { + case HookCategory.Code when _callback is Unicorn.CodeHook codeHook: + codeHook(_owner, address, size, _state); + break; + case HookCategory.Block when _callback is Unicorn.BlockHook blockHook: + blockHook(_owner, address, size, _state); + break; + default: + throw new InvalidOperationException($"Unsupported hook category {Category} for code invocation."); + } + } + catch (Exception ex) + { + HandleCallbackException(ex, Category == HookCategory.Code ? "Code" : "Block"); + } + } + + public void InvokeMemory(Unicorn.MemoryAccessType accessType, ulong address, int size, long value) + { + if (IsDisposed) + { + LogDisposedHook("Memory"); + return; + } + + if (_callback is not Unicorn.MemoryHook memHook) + { + LogInvalidCallback("Memory", nameof(Unicorn.MemoryHook)); + return; + } + + try + { + memHook(_owner, accessType, address, size, value, _state); + } + catch (Exception ex) + { + HandleCallbackException(ex, "Memory"); + } + } + + public bool InvokeEventMemory(Unicorn.MemoryAccessType accessType, ulong address, int size, long value) + { + if (IsDisposed) + { + LogDisposedHook("EventMemory"); + return true; + } + + if (_callback is not Unicorn.MemoryEventHook eventHook) + { + LogInvalidCallback("EventMemory", nameof(Unicorn.MemoryEventHook)); + return true; + } + + try + { + return eventHook(_owner, accessType, address, size, value, _state); + } + catch (Exception ex) + { + HandleCallbackException(ex, "EventMemory"); + return true; + } + } + + public void InvokeInterrupt(uint interruptNumber) + { + if (IsDisposed) + { + LogDisposedHook("Interrupt"); + return; + } + + if (_callback is not Unicorn.InterruptHook interruptHook) + { + LogInvalidCallback("Interrupt", nameof(Unicorn.InterruptHook)); + return; + } + + try + { + interruptHook(_owner, interruptNumber, _state); + } + catch (Exception ex) + { + HandleCallbackException(ex, "Interrupt"); + } + } + + public uint InvokeIn(uint port, int size) + { + if (IsDisposed) + { + LogDisposedHook("In"); + return 0; + } + + if (_callback is not Unicorn.InHook inHook) + { + LogInvalidCallback("In", nameof(Unicorn.InHook)); + return 0; + } + + try + { + return inHook(_owner, port, size, _state); + } + catch (Exception ex) + { + HandleCallbackException(ex, "In"); + return 0; + } + } + + public void InvokeOut(uint port, int size, uint value) + { + if (IsDisposed) + { + LogDisposedHook("Out"); + return; + } + + if (_callback is not Unicorn.OutHook outHook) + { + LogInvalidCallback("Out", nameof(Unicorn.OutHook)); + return; + } + + try + { + outHook(_owner, port, size, value, _state); + } + catch (Exception ex) + { + HandleCallbackException(ex, "Out"); + } + } + + public void InvokeSyscall() + { + if (IsDisposed) + { + LogDisposedHook("Syscall"); + return; + } + + if (_callback is not Unicorn.SyscallHook syscallHook) + { + LogInvalidCallback("Syscall", nameof(Unicorn.SyscallHook)); + return; + } + + try + { + syscallHook(_owner, _state); + } + catch (Exception ex) + { + HandleCallbackException(ex, "Syscall"); + } + } + + public bool InvokeInvalidInstruction() + { + if (IsDisposed) + { + LogDisposedHook("InvalidInstruction"); + return false; + } + + if (_callback is not Unicorn.InvalidInstructionHook invalidInsnHook) + { + LogInvalidCallback("InvalidInstruction", nameof(Unicorn.InvalidInstructionHook)); + return false; + } + + try + { + return invalidInsnHook(_owner, _state); + } + catch (Exception ex) + { + HandleCallbackException(ex, "InvalidInstruction"); + return false; + } + } + + private void HandleCallbackException(Exception exception, string hookType) + { + var logger = _owner.Logger; + var handling = _owner.Options.CallbackExceptionHandling; + + if (handling != CallbackExceptionHandling.Throw && logger == null) + { + handling = CallbackExceptionHandling.Throw; + } + + switch (handling) + { + case CallbackExceptionHandling.Throw: + throw exception; + case CallbackExceptionHandling.LogAndThrow: + logger!.LogError($"Exception in {hookType} hook callback", exception); + throw exception; + case CallbackExceptionHandling.LogAndContinue: + logger!.LogError($"Exception in {hookType} hook callback (continuing execution)", exception); + break; + } + } + + private void LogDisposedHook(string hookType) + { + if (_owner.Options.EnableVerboseDiagnostics) + { + _owner.Logger?.LogWarning($"{hookType} hook invoked but registration is disposed"); + } + } + + private void LogInvalidCallback(string hookType, string expectedType) + { + if (_owner.Options.EnableVerboseDiagnostics) + { + _owner.Logger?.LogWarning($"{hookType} hook invoked but callback is not of expected type {expectedType}"); + } + } + } +} diff --git a/UnicornNet/Hooks/IHookManager.cs b/UnicornNet/Hooks/IHookManager.cs new file mode 100644 index 0000000..35e5ef0 --- /dev/null +++ b/UnicornNet/Hooks/IHookManager.cs @@ -0,0 +1,10 @@ +using System; + +namespace UnicornNet; + +public interface IHookManager : IDisposable +{ + Unicorn.HookHandle AddHook(Unicorn.HookType type, Delegate callback, Unicorn.HookRange? range = null, object? state = null); + + void RemoveHook(Unicorn.HookHandle handle); +} diff --git a/UnicornNet/Hooks/Unicorn.Hooks.cs b/UnicornNet/Hooks/Unicorn.Hooks.cs new file mode 100644 index 0000000..6f1924b --- /dev/null +++ b/UnicornNet/Hooks/Unicorn.Hooks.cs @@ -0,0 +1,149 @@ +using System; + +namespace UnicornNet; + +public partial class Unicorn +{ + // Hook state is stored as object? because native callbacks return through IntPtr userData; + // value-type state is boxed, so generic hook overloads are intentionally not exposed. + + /// + /// Returns a fluent builder for registering multiple hooks + /// + public HookBuilder Hooks() + { + EnsureNotDisposed(); + return new HookBuilder(_hooks); + } + + public HookHandle AddCodeHook(CodeHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.Code, callback, range, state); + } + + public HookHandle AddBlockHook(BlockHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.Block, callback, range, state); + } + + public HookHandle AddMemReadHook(MemoryHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.MemRead, callback, range, state); + } + + public HookHandle AddMemWriteHook(MemoryHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.MemWrite, callback, range, state); + } + + public HookHandle AddInterruptHook(InterruptHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.Interrupt, callback, range, state); + } + + public HookHandle AddInHook(InHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.Instruction, callback, range, state); + } + + public HookHandle AddOutHook(OutHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.Instruction, callback, range, state); + } + + public HookHandle AddSyscallHook(SyscallHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.Instruction, callback, range, state); + } + + /// + /// Adds a hook that will be called when the emulator encounters an invalid instruction. + /// + /// + /// The callback to invoke when an invalid instruction is encountered. + /// Should return true to continue emulation, false to stop. + /// + /// The address range for which this hook is active (null for all addresses). + /// Optional user state to pass to the callback. + /// A handle that can be used to remove this hook later. + /// + /// + /// Error Handling: + /// + /// + /// If your callback throws an exception, the behavior depends on + /// : + /// + /// + /// + /// (default): The exception is rethrown + /// immediately + /// + /// + /// + /// + /// : The exception is logged then + /// rethrown + /// + /// + /// + /// + /// : The exception is logged and the + /// hook returns false (stops emulation) + /// + /// + /// + /// + /// + /// Diagnostic Context: + /// + /// + /// To get detailed information about the invalid instruction (PC, instruction bytes), + /// call from within your callback. + /// + /// + /// Example: + /// + /// + /// unicorn.AddInvalidInstructionHook((engine, state) => + /// { + /// var context = engine.GetInvalidInstructionContext(); + /// Console.WriteLine($"Invalid instruction: {context.GetDetailedMessage()}"); + /// return false; // Stop emulation + /// }); + /// + /// + public HookHandle AddInvalidInstructionHook(InvalidInstructionHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(HookType.InvalidInstruction, callback, range, state); + } + + /// + /// Adds an event memory hook that can monitor multiple event types using a hook-type bitmask + /// + public HookHandle AddEventMemHook(HookType eventTypes, MemoryEventHook callback, HookRange? range = null, object? state = null) + { + ArgumentNullException.ThrowIfNull(callback); + return _hooks.AddHook(eventTypes, callback, range, state); + } + + public void RemoveHook(HookHandle handle) + { + EnsureNotDisposed(); + _hooks.RemoveHook(handle); + } + + public void HookDel(HookHandle handle) + { + RemoveHook(handle); + } +} diff --git a/UnicornNet/Hooks/Unicorn.HooksInternal.cs b/UnicornNet/Hooks/Unicorn.HooksInternal.cs new file mode 100644 index 0000000..8d1946b --- /dev/null +++ b/UnicornNet/Hooks/Unicorn.HooksInternal.cs @@ -0,0 +1,44 @@ +namespace UnicornNet; + +public partial class Unicorn +{ + internal bool TrySimulateHook(HookHandle handle, ulong address, int size) + { + return _hooks.TrySimulateHook(handle, address, size); + } + + internal bool TrySimulateMemoryHook(HookHandle handle, MemoryAccessType accessType, ulong address, int size, long value) + { + return _hooks.TrySimulateMemoryHook(handle, accessType, address, size, value); + } + + internal bool TrySimulateEventMem(MemoryAccessType accessType, ulong address, int size, long value) + { + return _hooks.TrySimulateEventMem(accessType, address, size, value); + } + + internal bool TrySimulateInterruptHook(HookHandle handle, uint interruptNumber) + { + return _hooks.TrySimulateInterruptHook(handle, interruptNumber); + } + + internal bool TrySimulateInHook(HookHandle handle, uint port, int size, out uint value) + { + return _hooks.TrySimulateInHook(handle, port, size, out value); + } + + internal bool TrySimulateOutHook(HookHandle handle, uint port, int size, uint value) + { + return _hooks.TrySimulateOutHook(handle, port, size, value); + } + + internal bool TrySimulateSyscallHook(HookHandle handle) + { + return _hooks.TrySimulateSyscallHook(handle); + } + + internal bool TrySimulateInvalidInstructionHook(HookHandle handle, out bool continueExecution) + { + return _hooks.TrySimulateInvalidInstructionHook(handle, out continueExecution); + } +} diff --git a/UnicornNet/Memory/IMemoryManager.cs b/UnicornNet/Memory/IMemoryManager.cs new file mode 100644 index 0000000..2caaf75 --- /dev/null +++ b/UnicornNet/Memory/IMemoryManager.cs @@ -0,0 +1,16 @@ +using System; + +namespace UnicornNet; + +public interface IMemoryManager +{ + MemoryRegion Map(ulong address, ulong size, Unicorn.MemoryPermissions permissions); + + void Unmap(ulong address, ulong size); + + void Protect(ulong address, ulong size, Unicorn.MemoryPermissions permissions); + + void Write(ulong address, ReadOnlySpan data); + + void Read(ulong address, Span buffer); +} diff --git a/UnicornNet/Memory/MemoryManager.cs b/UnicornNet/Memory/MemoryManager.cs new file mode 100644 index 0000000..5e99580 --- /dev/null +++ b/UnicornNet/Memory/MemoryManager.cs @@ -0,0 +1,88 @@ +using System; + +namespace UnicornNet; + +internal sealed class MemoryManager : IMemoryManager +{ + private readonly Action _ensureNotDisposed; + private readonly Func _getEngineHandle; + private readonly IUnicornNativeProxy _native; + + public MemoryManager(IUnicornNativeProxy native, Func getEngineHandle, Action ensureNotDisposed) + { + ArgumentNullException.ThrowIfNull(native); + ArgumentNullException.ThrowIfNull(getEngineHandle); + ArgumentNullException.ThrowIfNull(ensureNotDisposed); + + _native = native; + _getEngineHandle = getEngineHandle; + _ensureNotDisposed = ensureNotDisposed; + } + + public MemoryRegion Map(ulong address, ulong size, Unicorn.MemoryPermissions permissions) + { + _ensureNotDisposed(); + var err = _native.MemMap(_getEngineHandle(), address, size, (uint)permissions); + if (err != 0) + { + throw new UnicornMemoryException((Unicorn.ErrorCode)err, "uc_mem_map", address, size); + } + + return new MemoryRegion(this, address, size, permissions); + } + + public void MapPtr(ulong address, ulong size, Unicorn.MemoryPermissions permissions, IntPtr pointer) + { + _ensureNotDisposed(); + if (pointer == IntPtr.Zero) + { + throw new ArgumentException("Pointer cannot be zero.", nameof(pointer)); + } + + var err = _native.MemMapPtr(_getEngineHandle(), address, size, (uint)permissions, pointer); + if (err != 0) + { + throw new UnicornMemoryException((Unicorn.ErrorCode)err, "uc_mem_map_ptr", address, size); + } + } + + public void Unmap(ulong address, ulong size) + { + _ensureNotDisposed(); + var err = _native.MemUnmap(_getEngineHandle(), address, size); + if (err != 0) + { + throw new UnicornMemoryException((Unicorn.ErrorCode)err, "uc_mem_unmap", address, size); + } + } + + public void Protect(ulong address, ulong size, Unicorn.MemoryPermissions permissions) + { + _ensureNotDisposed(); + var err = _native.MemProtect(_getEngineHandle(), address, size, (uint)permissions); + if (err != 0) + { + throw new UnicornMemoryException((Unicorn.ErrorCode)err, "uc_mem_protect", address, size); + } + } + + public void Write(ulong address, ReadOnlySpan data) + { + _ensureNotDisposed(); + var err = _native.MemWrite(_getEngineHandle(), address, data); + if (err != 0) + { + throw new UnicornMemoryException((Unicorn.ErrorCode)err, "uc_mem_write", address, (ulong)data.Length); + } + } + + public void Read(ulong address, Span buffer) + { + _ensureNotDisposed(); + var err = _native.MemRead(_getEngineHandle(), address, buffer); + if (err != 0) + { + throw new UnicornMemoryException((Unicorn.ErrorCode)err, "uc_mem_read", address, (ulong)buffer.Length); + } + } +} diff --git a/UnicornNet/MemoryRegion.cs b/UnicornNet/Memory/MemoryRegion.cs similarity index 85% rename from UnicornNet/MemoryRegion.cs rename to UnicornNet/Memory/MemoryRegion.cs index 1ef46bf..438e0b7 100644 --- a/UnicornNet/MemoryRegion.cs +++ b/UnicornNet/Memory/MemoryRegion.cs @@ -7,7 +7,7 @@ namespace UnicornNet; /// public readonly struct MemoryRegion : IDisposable { - private readonly Unicorn _engine; + private readonly IMemoryManager _memory; /// /// The base address of the memory region @@ -26,9 +26,11 @@ namespace UnicornNet; /// public Unicorn.MemoryPermissions Permissions { get; } - internal MemoryRegion(Unicorn engine, ulong address, ulong size, Unicorn.MemoryPermissions permissions) + internal MemoryRegion(IMemoryManager memory, ulong address, ulong size, Unicorn.MemoryPermissions permissions) { - _engine = engine; + ArgumentNullException.ThrowIfNull(memory); + + _memory = memory; Address = address; Size = size; Permissions = permissions; @@ -44,7 +46,7 @@ public readonly void Write(ReadOnlySpan data, ulong offset = 0) throw new ArgumentOutOfRangeException(nameof(data), "Data exceeds region bounds"); } - _engine.MemWrite(Address + offset, data); + _memory.Write(Address + offset, data); } /// @@ -57,7 +59,7 @@ public readonly void Read(Span buffer, ulong offset = 0) throw new ArgumentOutOfRangeException(nameof(buffer), "Buffer exceeds region bounds"); } - _engine.MemRead(Address + offset, buffer); + _memory.Read(Address + offset, buffer); } /// @@ -65,7 +67,7 @@ public readonly void Read(Span buffer, ulong offset = 0) /// public readonly void Protect(Unicorn.MemoryPermissions permissions) { - _engine.MemProtect(Address, Size, permissions); + _memory.Protect(Address, Size, permissions); } /// @@ -73,7 +75,7 @@ public readonly void Protect(Unicorn.MemoryPermissions permissions) /// public readonly void Dispose() { - _engine.MemUnmap(Address, Size); + _memory.Unmap(Address, Size); } /// @@ -99,4 +101,4 @@ public readonly bool Contains(ulong address, ulong size) { return address >= Address && address + size <= EndAddress; } -} \ No newline at end of file +} diff --git a/UnicornNet/Memory/Unicorn.Memory.cs b/UnicornNet/Memory/Unicorn.Memory.cs new file mode 100644 index 0000000..4c0b8ee --- /dev/null +++ b/UnicornNet/Memory/Unicorn.Memory.cs @@ -0,0 +1,59 @@ +using System; + +namespace UnicornNet; + +public partial class Unicorn +{ + /// + /// Map a memory region and return a MemoryRegion helper for managing it + /// + public MemoryRegion MapRegion(ulong address, ulong size, MemoryPermissions permissions) + { + return _memory.Map(address, size, permissions); + } + + public void MemMap(ulong address, ulong size, MemoryPermissions permissions) + { + _memory.Map(address, size, permissions); + } + + public void MemMap(ulong address, ulong size, uint permissions) + { + MemMap(address, size, (MemoryPermissions)permissions); + } + + public void MemMapPtr(ulong address, ulong size, MemoryPermissions permissions, IntPtr pointer) + { + _memory.MapPtr(address, size, permissions, pointer); + } + + public void MemMapPtr(ulong address, ulong size, uint permissions, IntPtr pointer) + { + MemMapPtr(address, size, (MemoryPermissions)permissions, pointer); + } + + public void MemUnmap(ulong address, ulong size) + { + _memory.Unmap(address, size); + } + + public void MemProtect(ulong address, ulong size, MemoryPermissions permissions) + { + _memory.Protect(address, size, permissions); + } + + public void MemProtect(ulong address, ulong size, uint permissions) + { + MemProtect(address, size, (MemoryPermissions)permissions); + } + + public void MemWrite(ulong address, ReadOnlySpan data) + { + _memory.Write(address, data); + } + + public void MemRead(ulong address, Span buffer) + { + _memory.Read(address, buffer); + } +} diff --git a/UnicornNet/Registers/IRegisterBank.cs b/UnicornNet/Registers/IRegisterBank.cs new file mode 100644 index 0000000..3be3593 --- /dev/null +++ b/UnicornNet/Registers/IRegisterBank.cs @@ -0,0 +1,16 @@ +using System; + +namespace UnicornNet; + +public interface IRegisterBank +{ + void Write(int registerId, ReadOnlySpan value); + + void Write(int registerId, T value) + where T : unmanaged; + + void Read(int registerId, Span destination); + + T Read(int registerId) + where T : unmanaged; +} diff --git a/UnicornNet/Registers/RegisterBank.cs b/UnicornNet/Registers/RegisterBank.cs new file mode 100644 index 0000000..39f0447 --- /dev/null +++ b/UnicornNet/Registers/RegisterBank.cs @@ -0,0 +1,106 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace UnicornNet; + +internal sealed class RegisterBank : IRegisterBank +{ + private readonly Action _ensureNotDisposed; + private readonly Func _getEngineHandle; + private readonly IUnicornNativeProxy _native; + + public RegisterBank(IUnicornNativeProxy native, Func getEngineHandle, Action ensureNotDisposed) + { + ArgumentNullException.ThrowIfNull(native); + ArgumentNullException.ThrowIfNull(getEngineHandle); + ArgumentNullException.ThrowIfNull(ensureNotDisposed); + + _native = native; + _getEngineHandle = getEngineHandle; + _ensureNotDisposed = ensureNotDisposed; + } + + public void Write(int registerId, ReadOnlySpan value) + { + _ensureNotDisposed(); + if (value.IsEmpty) + { + throw new ArgumentException("Value span cannot be empty.", nameof(value)); + } + + var err = _native.RegWrite(_getEngineHandle(), registerId, value); + if (err != 0) + { + throw new UnicornEngineException((Unicorn.ErrorCode)err, "uc_reg_write"); + } + } + + public void Write(TRegister register, ReadOnlySpan value) + where TRegister : struct, Enum + { + Write(NormalizeRegisterId(register), value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(int registerId, T value) + where T : unmanaged + { + var span = MemoryMarshal.CreateReadOnlySpan(ref value, 1); + Write(registerId, MemoryMarshal.AsBytes(span)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(TRegister register, TValue value) + where TRegister : struct, Enum + where TValue : unmanaged + { + Write(NormalizeRegisterId(register), value); + } + + public void Read(int registerId, Span destination) + { + _ensureNotDisposed(); + if (destination.IsEmpty) + { + throw new ArgumentException("Destination span cannot be empty.", nameof(destination)); + } + + var err = _native.RegRead(_getEngineHandle(), registerId, destination); + if (err != 0) + { + throw new UnicornEngineException((Unicorn.ErrorCode)err, "uc_reg_read"); + } + } + + public void Read(TRegister register, Span destination) + where TRegister : struct, Enum + { + Read(NormalizeRegisterId(register), destination); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Read(int registerId) + where T : unmanaged + { + T value = default; + var span = MemoryMarshal.CreateSpan(ref value, 1); + Read(registerId, MemoryMarshal.AsBytes(span)); + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Read(TRegister register) + where TRegister : struct, Enum + where TValue : unmanaged + { + return Read(NormalizeRegisterId(register)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NormalizeRegisterId(TRegister register) + where TRegister : struct, Enum + { + return Unsafe.As(ref register); + } +} diff --git a/UnicornNet/Unicorn.Registers.Constants.cs b/UnicornNet/Registers/Unicorn.Registers.Constants.cs similarity index 100% rename from UnicornNet/Unicorn.Registers.Constants.cs rename to UnicornNet/Registers/Unicorn.Registers.Constants.cs diff --git a/UnicornNet/Unicorn.Registers.cs b/UnicornNet/Registers/Unicorn.Registers.cs similarity index 77% rename from UnicornNet/Unicorn.Registers.cs rename to UnicornNet/Registers/Unicorn.Registers.cs index a5e7081..7c9df9f 100644 --- a/UnicornNet/Unicorn.Registers.cs +++ b/UnicornNet/Registers/Unicorn.Registers.cs @@ -1,92 +1,55 @@ using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace UnicornNet; public partial class Unicorn { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int NormalizeRegisterId(TRegister register) - where TRegister : struct, Enum - { - return Unsafe.As(ref register); - } - public void RegWrite(int registerId, ReadOnlySpan value) { - EnsureNotDisposed(); - if (value.IsEmpty) - { - throw new ArgumentException("Value span cannot be empty.", nameof(value)); - } - - var err = _native.RegWrite(EngineHandle, registerId, value); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_reg_write"); - } + _registers.Write(registerId, value); } public void RegWrite(TRegister register, ReadOnlySpan value) where TRegister : struct, Enum { - RegWrite(NormalizeRegisterId(register), value); + _registers.Write(register, value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RegWrite(int registerId, T value) where T : unmanaged { - var span = MemoryMarshal.CreateReadOnlySpan(ref value, 1); - RegWrite(registerId, MemoryMarshal.AsBytes(span)); + _registers.Write(registerId, value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RegWrite(TRegister register, TValue value) where TRegister : struct, Enum where TValue : unmanaged { - RegWrite(NormalizeRegisterId(register), value); + _registers.Write(register, value); } public void RegRead(int registerId, Span destination) { - EnsureNotDisposed(); - if (destination.IsEmpty) - { - throw new ArgumentException("Destination span cannot be empty.", nameof(destination)); - } - - var err = _native.RegRead(EngineHandle, registerId, destination); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_reg_read"); - } + _registers.Read(registerId, destination); } public void RegRead(TRegister register, Span destination) where TRegister : struct, Enum { - RegRead(NormalizeRegisterId(register), destination); + _registers.Read(register, destination); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public T RegRead(int registerId) where T : unmanaged { - T value = default; - var span = MemoryMarshal.CreateSpan(ref value, 1); - RegRead(registerId, MemoryMarshal.AsBytes(span)); - return value; + return _registers.Read(registerId); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue RegRead(TRegister register) where TRegister : struct, Enum where TValue : unmanaged { - return RegRead(NormalizeRegisterId(register)); + return _registers.Read(register); } #region Register Enum Overloads @@ -262,4 +225,4 @@ public T RegRead(Registers.X86 register) where T : unmanaged } #endregion -} \ No newline at end of file +} diff --git a/UnicornNet/Unicorn.Constants.cs b/UnicornNet/Unicorn.Constants.cs index 254f934..888f7b2 100644 --- a/UnicornNet/Unicorn.Constants.cs +++ b/UnicornNet/Unicorn.Constants.cs @@ -8,7 +8,7 @@ public partial class Unicorn // Non-generic delegates for backward compatibility public delegate void BlockHook(Unicorn engine, ulong address, int size, object? state); - // Generic delegates to avoid boxing value type states + // Generic delegate types are retained for source compatibility; hook registration stores state as object?. public delegate void BlockHook(Unicorn engine, ulong address, int size, TState state); public delegate void CodeHook(Unicorn engine, ulong address, int size, object? state); @@ -256,13 +256,33 @@ public enum TlbType : uint /// private const int X86InstructionSyscall = 699; - public readonly record struct ControlCommand(uint Value) + public readonly record struct ControlCommand { private const int ArgumentCountShift = 26; private const int IoShift = 30; private const uint TypeMask = (1u << ArgumentCountShift) - 1; private const uint ArgumentMask = 0xFu; private const uint IoMask = 0x3u; + private readonly nint[]? _arguments; + + public ControlCommand(uint value) + { + Value = value; + _arguments = null; + } + + private ControlCommand(uint value, nint[] arguments) + { + Value = value; + _arguments = arguments; + } + + public uint Value { get; } + + public ReadOnlySpan Arguments + { + get => _arguments; + } public ControlType Type { @@ -292,26 +312,61 @@ public static ControlCommand Create(ControlType type, int argumentCount, Control return new ControlCommand(value); } + public static ControlCommand Create(ControlType type, ReadOnlySpan arguments, ControlIo access) + { + return Create(type, arguments.Length, access).WithArguments(arguments); + } + + public ControlCommand WithArguments(ReadOnlySpan arguments) + { + if (arguments.Length != ArgumentCount) + { + throw new ArgumentException("Argument length must match the command argument count.", nameof(arguments)); + } + + return new ControlCommand(Value, arguments.ToArray()); + } + public static ControlCommand None(ControlType type, int argumentCount) { return Create(type, argumentCount, ControlIo.None); } + public static ControlCommand None(ControlType type, ReadOnlySpan arguments) + { + return Create(type, arguments, ControlIo.None); + } + public static ControlCommand Read(ControlType type, int argumentCount) { return Create(type, argumentCount, ControlIo.Read); } + public static ControlCommand Read(ControlType type, ReadOnlySpan arguments) + { + return Create(type, arguments, ControlIo.Read); + } + public static ControlCommand Write(ControlType type, int argumentCount) { return Create(type, argumentCount, ControlIo.Write); } + public static ControlCommand Write(ControlType type, ReadOnlySpan arguments) + { + return Create(type, arguments, ControlIo.Write); + } + public static ControlCommand ReadWrite(ControlType type, int argumentCount) { return Create(type, argumentCount, ControlIo.ReadWrite); } + public static ControlCommand ReadWrite(ControlType type, ReadOnlySpan arguments) + { + return Create(type, arguments, ControlIo.ReadWrite); + } + public static implicit operator ControlCommand(uint value) { return new ControlCommand(value); @@ -346,4 +401,4 @@ public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan return Value.TryFormat(destination, out charsWritten, format, provider ?? CultureInfo.InvariantCulture); } } -} \ No newline at end of file +} diff --git a/UnicornNet/Unicorn.Core.cs b/UnicornNet/Unicorn.Core.cs index a1c87fa..74d577f 100644 --- a/UnicornNet/Unicorn.Core.cs +++ b/UnicornNet/Unicorn.Core.cs @@ -4,8 +4,12 @@ namespace UnicornNet; public partial class Unicorn : IDisposable { + private readonly ControlEngine _control; private readonly SafeEngineHandle _engineHandle; + private readonly HookManager _hooks; + private readonly MemoryManager _memory; private readonly IUnicornNativeProxy _native; + private readonly RegisterBank _registers; private bool _disposed; public Unicorn(Architecture architecture, Mode mode) @@ -40,6 +44,10 @@ internal Unicorn(Architecture architecture, Mode mode, UnicornOptions options, I } _engineHandle = new SafeEngineHandle(handle, _native); + _control = new ControlEngine(_native, () => EngineHandle, EnsureNotDisposed); + _hooks = new HookManager(this, _native, () => EngineHandle, EnsureNotDisposed); + _memory = new MemoryManager(_native, () => EngineHandle, EnsureNotDisposed); + _registers = new RegisterBank(_native, () => EngineHandle, EnsureNotDisposed); } /// @@ -77,7 +85,7 @@ public void Dispose() return; } - ClearHooks(); + _hooks?.Dispose(); _engineHandle?.Dispose(); _disposed = true; GC.SuppressFinalize(this); @@ -93,201 +101,6 @@ private void EnsureNotDisposed() ObjectDisposedException.ThrowIf(_disposed, nameof(Unicorn)); } - public void Control(ControlCommand command) - { - EnsureNotDisposed(); - var err = _native.Control(EngineHandle, command.Value, ReadOnlySpan.Empty); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_ctl"); - } - } - - public void Control(ControlCommand command, nint arg1) - { - EnsureNotDisposed(); - ReadOnlySpan args = [arg1]; - var err = _native.Control(EngineHandle, command.Value, args); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_ctl"); - } - } - - public void Control(ControlCommand command, nint arg1, nint arg2) - { - EnsureNotDisposed(); - ReadOnlySpan args = [arg1, arg2]; - var err = _native.Control(EngineHandle, command.Value, args); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_ctl"); - } - } - - public void Control(ControlCommand command, nint arg1, nint arg2, nint arg3) - { - EnsureNotDisposed(); - ReadOnlySpan args = [arg1, arg2, arg3]; - var err = _native.Control(EngineHandle, command.Value, args); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_ctl"); - } - } - - public void Control(ControlCommand command, nint arg1, nint arg2, nint arg3, nint arg4) - { - EnsureNotDisposed(); - ReadOnlySpan args = [arg1, arg2, arg3, arg4]; - var err = _native.Control(EngineHandle, command.Value, args); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_ctl"); - } - } - - public void Control(ControlCommand command, ReadOnlySpan arguments) - { - EnsureNotDisposed(); - var err = _native.Control(EngineHandle, command.Value, arguments); - if (err != 0) - { - throw new UnicornEngineException((ErrorCode)err, "uc_ctl"); - } - } - - public void Control(ControlType type, ControlIo access) - { - var command = ControlCommand.Create(type, 0, access); - Control(command); - } - - public void Control(ControlType type, ControlIo access, nint arg1) - { - var command = ControlCommand.Create(type, 1, access); - Control(command, arg1); - } - - public void Control(ControlType type, ControlIo access, nint arg1, nint arg2) - { - var command = ControlCommand.Create(type, 2, access); - Control(command, arg1, arg2); - } - - public void Control(ControlType type, ControlIo access, nint arg1, nint arg2, nint arg3) - { - var command = ControlCommand.Create(type, 3, access); - Control(command, arg1, arg2, arg3); - } - - public void Control(ControlType type, ControlIo access, nint arg1, nint arg2, nint arg3, nint arg4) - { - var command = ControlCommand.Create(type, 4, access); - Control(command, arg1, arg2, arg3, arg4); - } - - public void Control(ControlType type, ControlIo access, ReadOnlySpan arguments) - { - var command = ControlCommand.Create(type, arguments.Length, access); - Control(command, arguments); - } - - public void ControlRead(ControlType type) - { - Control(type, ControlIo.Read); - } - - public void ControlRead(ControlType type, nint arg1) - { - Control(type, ControlIo.Read, arg1); - } - - public void ControlRead(ControlType type, nint arg1, nint arg2) - { - Control(type, ControlIo.Read, arg1, arg2); - } - - public void ControlRead(ControlType type, nint arg1, nint arg2, nint arg3) - { - Control(type, ControlIo.Read, arg1, arg2, arg3); - } - - public void ControlRead(ControlType type, nint arg1, nint arg2, nint arg3, nint arg4) - { - Control(type, ControlIo.Read, arg1, arg2, arg3, arg4); - } - - public void ControlRead(ControlType type, ReadOnlySpan arguments) - { - Control(type, ControlIo.Read, arguments); - } - - public void ControlWrite(ControlType type) - { - Control(type, ControlIo.Write); - } - - public void ControlWrite(ControlType type, nint arg1) - { - Control(type, ControlIo.Write, arg1); - } - - public void ControlWrite(ControlType type, nint arg1, nint arg2) - { - Control(type, ControlIo.Write, arg1, arg2); - } - - public void ControlWrite(ControlType type, nint arg1, nint arg2, nint arg3) - { - Control(type, ControlIo.Write, arg1, arg2, arg3); - } - - public void ControlWrite(ControlType type, nint arg1, nint arg2, nint arg3, nint arg4) - { - Control(type, ControlIo.Write, arg1, arg2, arg3, arg4); - } - - public void ControlWrite(ControlType type, ReadOnlySpan arguments) - { - Control(type, ControlIo.Write, arguments); - } - - public void ControlReadWrite(ControlType type) - { - Control(type, ControlIo.ReadWrite); - } - - public void ControlReadWrite(ControlType type, nint arg1) - { - Control(type, ControlIo.ReadWrite, arg1); - } - - public void ControlReadWrite(ControlType type, nint arg1, nint arg2) - { - Control(type, ControlIo.ReadWrite, arg1, arg2); - } - - public void ControlReadWrite(ControlType type, nint arg1, nint arg2, nint arg3) - { - Control(type, ControlIo.ReadWrite, arg1, arg2, arg3); - } - - public void ControlReadWrite(ControlType type, nint arg1, nint arg2, nint arg3, nint arg4) - { - Control(type, ControlIo.ReadWrite, arg1, arg2, arg3, arg4); - } - - public void ControlReadWrite(ControlType type, ReadOnlySpan arguments) - { - Control(type, ControlIo.ReadWrite, arguments); - } - - public void ControlNone(ControlType type) - { - Control(type, ControlIo.None); - } - /// /// Removes cached translation blocks for the specified address range. /// This is useful when code has been modified at runtime and the translation cache needs to be invalidated. @@ -342,28 +155,7 @@ public ErrorCode GetErrorState() { EnsureNotDisposed(); - var totalCount = _hookRegistry.Count; - var disposedCount = 0; - - foreach (var registration in _hookRegistry.Values) - { - // Check if the registration is disposed by trying to access its handle - // We can't directly check _disposed as it's private, so we check if handle is empty - // and if the GCHandle is still allocated - try - { - if (registration.Handle.IsEmpty) - { - disposedCount++; - } - } - catch - { - disposedCount++; - } - } - - var isHealthy = disposedCount == 0; + var (isHealthy, disposedCount, totalCount) = _hooks.Validate(); if (!isHealthy && Logger != null) { @@ -372,4 +164,4 @@ public ErrorCode GetErrorState() return (isHealthy, disposedCount, totalCount); } -} \ No newline at end of file +} diff --git a/UnicornNet/Unicorn.HookRegistration.cs b/UnicornNet/Unicorn.HookRegistration.cs deleted file mode 100644 index ccb4e51..0000000 --- a/UnicornNet/Unicorn.HookRegistration.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace UnicornNet; - -public partial class Unicorn -{ - private enum HookCategory - { - Code, - Block, - Memory, - EventMemory, - Interrupt, - In, - Out, - Syscall, - InvalidInstruction - } - - /// - /// Manages the lifecycle of a hook registration. - /// WARNING: This class is NOT thread-safe. Hook callbacks and disposal must not be called concurrently. - /// If you need to use hooks from multiple threads, ensure proper synchronization externally. - /// - private sealed class HookRegistration : IDisposable - { - private readonly Delegate _callback; - private readonly Unicorn _owner; - private readonly object? _state; - private bool _disposed; - private GCHandle _gcHandle; - - public HookRegistration(Unicorn owner, HookType type, HookCategory category, Delegate callback, object? state, MemoryAccessType? accessType = null) - { - ArgumentNullException.ThrowIfNull(callback); - _owner = owner; - Type = type; - Category = category; - _callback = callback; - _state = state; - AccessType = accessType; - _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); - } - - public HookHandle Handle { get; private set; } - - public HookCategory Category { get; } - - public HookType Type { get; } - - public MemoryAccessType? AccessType { get; } - - public IntPtr UserDataPointer - { - get => GCHandle.ToIntPtr(_gcHandle); - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - if (_gcHandle.IsAllocated) - { - _gcHandle.Free(); - } - - _disposed = true; - } - - public void SetHandle(HookHandle handle) - { - Handle = handle; - } - - private void HandleCallbackException(Exception exception, string hookType) - { - var logger = _owner.Logger; - var handling = _owner.Options.CallbackExceptionHandling; - - // If no logger configured but user wants logging, fall back to throwing - if (handling != CallbackExceptionHandling.Throw && logger == null) - { - handling = CallbackExceptionHandling.Throw; - } - - switch (handling) - { - case CallbackExceptionHandling.Throw: - throw exception; - - case CallbackExceptionHandling.LogAndThrow: - logger!.LogError($"Exception in {hookType} hook callback", exception); - throw exception; - - case CallbackExceptionHandling.LogAndContinue: - logger!.LogError($"Exception in {hookType} hook callback (continuing execution)", exception); - break; - } - } - - private void LogDisposedHook(string hookType) - { - if (_owner.Options.EnableVerboseDiagnostics) - { - _owner.Logger?.LogWarning($"{hookType} hook invoked but registration is disposed"); - } - } - - private void LogInvalidCallback(string hookType, string expectedType) - { - if (_owner.Options.EnableVerboseDiagnostics) - { - _owner.Logger?.LogWarning($"{hookType} hook invoked but callback is not of expected type {expectedType}"); - } - } - - public void InvokeCode(ulong address, int size) - { - if (_disposed) - { - LogDisposedHook("Code/Block"); - return; - } - - try - { - switch (Category) - { - case HookCategory.Code when _callback is CodeHook codeHook: - codeHook(_owner, address, size, _state); - break; - case HookCategory.Block when _callback is BlockHook blockHook: - blockHook(_owner, address, size, _state); - break; - case HookCategory.Memory: - case HookCategory.EventMemory: - case HookCategory.Interrupt: - case HookCategory.In: - case HookCategory.Out: - case HookCategory.Syscall: - default: - throw new InvalidOperationException($"Unsupported hook category {Category} for code invocation."); - } - } - catch (Exception ex) - { - HandleCallbackException(ex, Category == HookCategory.Code ? "Code" : "Block"); - } - } - - public void InvokeMemory(MemoryAccessType accessType, ulong address, int size, long value) - { - if (_disposed) - { - LogDisposedHook("Memory"); - return; - } - - if (_callback is not MemoryHook memHook) - { - LogInvalidCallback("Memory", nameof(MemoryHook)); - return; - } - - try - { - memHook(_owner, accessType, address, size, value, _state); - } - catch (Exception ex) - { - HandleCallbackException(ex, "Memory"); - } - } - - public bool InvokeEventMemory(MemoryAccessType accessType, ulong address, int size, long value) - { - if (_disposed) - { - LogDisposedHook("EventMemory"); - return true; - } - - if (_callback is not MemoryEventHook eventHook) - { - LogInvalidCallback("EventMemory", nameof(MemoryEventHook)); - return true; - } - - try - { - return eventHook(_owner, accessType, address, size, value, _state); - } - catch (Exception ex) - { - HandleCallbackException(ex, "EventMemory"); - return true; // Default return if LogAndContinue - } - } - - public void InvokeInterrupt(uint interruptNumber) - { - if (_disposed) - { - LogDisposedHook("Interrupt"); - return; - } - - if (_callback is not InterruptHook interruptHook) - { - LogInvalidCallback("Interrupt", nameof(InterruptHook)); - return; - } - - try - { - interruptHook(_owner, interruptNumber, _state); - } - catch (Exception ex) - { - HandleCallbackException(ex, "Interrupt"); - } - } - - public uint InvokeIn(uint port, int size) - { - if (_disposed) - { - LogDisposedHook("In"); - return 0; - } - - if (_callback is not InHook inHook) - { - LogInvalidCallback("In", nameof(InHook)); - return 0; - } - - try - { - return inHook(_owner, port, size, _state); - } - catch (Exception ex) - { - HandleCallbackException(ex, "In"); - return 0; // Default return if LogAndContinue - } - } - - public void InvokeOut(uint port, int size, uint value) - { - if (_disposed) - { - LogDisposedHook("Out"); - return; - } - - if (_callback is not OutHook outHook) - { - LogInvalidCallback("Out", nameof(OutHook)); - return; - } - - try - { - outHook(_owner, port, size, value, _state); - } - catch (Exception ex) - { - HandleCallbackException(ex, "Out"); - } - } - - public void InvokeSyscall() - { - if (_disposed) - { - LogDisposedHook("Syscall"); - return; - } - - if (_callback is not SyscallHook syscallHook) - { - LogInvalidCallback("Syscall", nameof(SyscallHook)); - return; - } - - try - { - syscallHook(_owner, _state); - } - catch (Exception ex) - { - HandleCallbackException(ex, "Syscall"); - } - } - - public bool InvokeInvalidInstruction() - { - if (_disposed) - { - LogDisposedHook("InvalidInstruction"); - return false; - } - - if (_callback is not InvalidInstructionHook invalidInsnHook) - { - LogInvalidCallback("InvalidInstruction", nameof(InvalidInstructionHook)); - return false; - } - - try - { - return invalidInsnHook(_owner, _state); - } - catch (Exception ex) - { - HandleCallbackException(ex, "InvalidInstruction"); - return false; // Default return if LogAndContinue - } - } - } -} \ No newline at end of file diff --git a/UnicornNet/Unicorn.Hooks.cs b/UnicornNet/Unicorn.Hooks.cs deleted file mode 100644 index 5c11034..0000000 --- a/UnicornNet/Unicorn.Hooks.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; - -namespace UnicornNet; - -public partial class Unicorn -{ - /// - /// Returns a fluent builder for registering multiple hooks - /// - public HookBuilder Hooks() - { - EnsureNotDisposed(); - return new HookBuilder(this); - } - - public HookHandle AddCodeHook(CodeHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterHook(HookType.Code, callback, state, range); - } - - /// - /// Adds a code hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddCodeHook(CodeHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - // Wrap the generic callback to bridge to the non-generic internal implementation - CodeHook wrapper = (engine, address, size, boxedState) => callback(engine, address, size, (TState)boxedState!); - return RegisterHook(HookType.Code, wrapper, state, range); - } - - public HookHandle AddBlockHook(BlockHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterHook(HookType.Block, callback, state, range); - } - - /// - /// Adds a block hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddBlockHook(BlockHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - BlockHook wrapper = (engine, address, size, boxedState) => callback(engine, address, size, (TState)boxedState!); - return RegisterHook(HookType.Block, wrapper, state, range); - } - - public HookHandle AddMemReadHook(MemoryHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterMemoryHook(HookType.MemRead, MemoryAccessType.Read, callback, range, state); - } - - /// - /// Adds a memory read hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddMemReadHook(MemoryHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - MemoryHook wrapper = (engine, accessType, address, size, value, boxedState) => callback(engine, accessType, address, size, value, (TState)boxedState!); - return RegisterMemoryHook(HookType.MemRead, MemoryAccessType.Read, wrapper, range, state); - } - - public HookHandle AddMemWriteHook(MemoryHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterMemoryHook(HookType.MemWrite, MemoryAccessType.Write, callback, range, state); - } - - /// - /// Adds a memory write hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddMemWriteHook(MemoryHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - MemoryHook wrapper = (engine, accessType, address, size, value, boxedState) => callback(engine, accessType, address, size, value, (TState)boxedState!); - return RegisterMemoryHook(HookType.MemWrite, MemoryAccessType.Write, wrapper, range, state); - } - - public HookHandle AddInterruptHook(InterruptHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterInterruptHook(callback, range, state); - } - - /// - /// Adds an interrupt hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddInterruptHook(InterruptHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - InterruptHook wrapper = (engine, interruptNumber, boxedState) => callback(engine, interruptNumber, (TState)boxedState!); - return RegisterInterruptHook(wrapper, range, state); - } - - public HookHandle AddInHook(InHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterInHook(callback, range, state); - } - - /// - /// Adds an IN instruction hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddInHook(InHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - InHook wrapper = (engine, port, size, boxedState) => callback(engine, port, size, (TState)boxedState!); - return RegisterInHook(wrapper, range, state); - } - - public HookHandle AddOutHook(OutHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterOutHook(callback, range, state); - } - - /// - /// Adds an OUT instruction hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddOutHook(OutHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - OutHook wrapper = (engine, port, size, value, boxedState) => callback(engine, port, size, value, (TState)boxedState!); - return RegisterOutHook(wrapper, range, state); - } - - public HookHandle AddSyscallHook(SyscallHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterSyscallHook(callback, range, state); - } - - /// - /// Adds a syscall hook with a strongly-typed state parameter to avoid boxing - /// - public HookHandle AddSyscallHook(SyscallHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - SyscallHook wrapper = (engine, boxedState) => callback(engine, (TState)boxedState!); - return RegisterSyscallHook(wrapper, range, state); - } - - /// - /// Adds a hook that will be called when the emulator encounters an invalid instruction. - /// - /// - /// The callback to invoke when an invalid instruction is encountered. - /// Should return true to continue emulation, false to stop. - /// - /// The address range for which this hook is active (null for all addresses). - /// Optional user state to pass to the callback. - /// A handle that can be used to remove this hook later. - /// - /// - /// Error Handling: - /// - /// - /// If your callback throws an exception, the behavior depends on - /// : - /// - /// - /// - /// (default): The exception is rethrown - /// immediately - /// - /// - /// - /// - /// : The exception is logged then - /// rethrown - /// - /// - /// - /// - /// : The exception is logged and the - /// hook returns false (stops emulation) - /// - /// - /// - /// - /// - /// Diagnostic Context: - /// - /// - /// To get detailed information about the invalid instruction (PC, instruction bytes), - /// call from within your callback. - /// - /// - /// Example: - /// - /// - /// unicorn.AddInvalidInstructionHook((engine, state) => - /// { - /// var context = engine.GetInvalidInstructionContext(); - /// Console.WriteLine($"Invalid instruction: {context.GetDetailedMessage()}"); - /// return false; // Stop emulation - /// }); - /// - /// - public HookHandle AddInvalidInstructionHook(InvalidInstructionHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterInvalidInstructionHook(callback, range, state); - } - - /// - /// Adds an invalid instruction hook with a strongly-typed state parameter to avoid boxing. - /// - /// The type of the state parameter. - /// - /// The callback to invoke when an invalid instruction is encountered. - /// Should return true to continue emulation, false to stop. - /// - /// The address range for which this hook is active (null for all addresses). - /// Optional user state to pass to the callback. - /// A handle that can be used to remove this hook later. - /// - /// See for detailed - /// documentation - /// on error handling and diagnostic capabilities. - /// - public HookHandle AddInvalidInstructionHook(InvalidInstructionHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - InvalidInstructionHook wrapper = (engine, boxedState) => callback(engine, (TState)boxedState!); - return RegisterInvalidInstructionHook(wrapper, range, state); - } - - /// - /// Adds an event memory hook that can monitor multiple event types using a hook-type bitmask - /// - public HookHandle AddEventMemHook(HookType eventTypes, MemoryEventHook callback, HookRange? range = null, object? state = null) - { - ArgumentNullException.ThrowIfNull(callback); - return RegisterEventMemHook(eventTypes, callback, range, state); - } - - /// - /// Adds a memory event hook with a strongly-typed state parameter that can listen to multiple hook types - /// - public HookHandle AddEventMemHook(HookType eventTypes, MemoryEventHook callback, HookRange? range = null, TState? state = default) - { - ArgumentNullException.ThrowIfNull(callback); - MemoryEventHook wrapper = (engine, accessType2, address, size, value, boxedState) => callback(engine, accessType2, address, size, value, (TState)boxedState!); - return RegisterEventMemHook(eventTypes, wrapper, range, state); - } - - public void RemoveHook(HookHandle handle) - { - EnsureNotDisposed(); - RemoveHookInternal(handle, false); - } - - public void HookDel(HookHandle handle) - { - RemoveHook(handle); - } -} \ No newline at end of file diff --git a/UnicornNet/Unicorn.HooksInternal.cs b/UnicornNet/Unicorn.HooksInternal.cs deleted file mode 100644 index 1467cf8..0000000 --- a/UnicornNet/Unicorn.HooksInternal.cs +++ /dev/null @@ -1,392 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace UnicornNet; - -public partial class Unicorn -{ - private const HookType EventMemoryHookMask = HookType.MemReadUnmapped - | HookType.MemWriteUnmapped - | HookType.MemFetchUnmapped - | HookType.MemReadProt - | HookType.MemWriteProt - | HookType.MemFetchProt; - - private readonly ConcurrentDictionary> _eventMemRegistrations = new(); - private readonly ConcurrentDictionary _hookRegistry = new(); - - internal bool TrySimulateHook(HookHandle handle, ulong address, int size) - { - if (!TryGetRegistration(handle, out var registration)) - { - return false; - } - - if (registration.Category is not (HookCategory.Code or HookCategory.Block)) - return false; - - registration.InvokeCode(address, size); - return true; - } - - internal bool TrySimulateMemoryHook(HookHandle handle, MemoryAccessType accessType, ulong address, int size, long value) - { - if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Memory) - { - return false; - } - - registration.InvokeMemory(accessType, address, size, value); - return true; - } - - internal bool TrySimulateEventMem(MemoryAccessType accessType, ulong address, int size, long value) - { - if (!_eventMemRegistrations.TryGetValue(accessType, out var handles) || handles.IsEmpty) - { - return false; - } - - var invoked = false; - foreach (var handleEntry in handles) - { - if (!_hookRegistry.TryGetValue(handleEntry.Key, out var registration)) - continue; - invoked = true; - if (!registration.InvokeEventMemory(accessType, address, size, value)) - { - break; - } - } - - return invoked; - } - - internal bool TrySimulateInterruptHook(HookHandle handle, uint interruptNumber) - { - if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Interrupt) - { - return false; - } - - registration.InvokeInterrupt(interruptNumber); - return true; - } - - internal bool TrySimulateInHook(HookHandle handle, uint port, int size, out uint value) - { - if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.In) - { - value = 0; - return false; - } - - value = registration.InvokeIn(port, size); - return true; - } - - internal bool TrySimulateOutHook(HookHandle handle, uint port, int size, uint value) - { - if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Out) - { - return false; - } - - registration.InvokeOut(port, size, value); - return true; - } - - internal bool TrySimulateSyscallHook(HookHandle handle) - { - if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.Syscall) - { - return false; - } - - registration.InvokeSyscall(); - return true; - } - - internal bool TrySimulateInvalidInstructionHook(HookHandle handle, out bool continueExecution) - { - continueExecution = false; - if (!TryGetRegistration(handle, out var registration) || registration.Category != HookCategory.InvalidInstruction) - { - return false; - } - - continueExecution = registration.InvokeInvalidInstruction(); - return true; - } - - private HookHandle RegisterHook(HookType type, Delegate callback, object? state, HookRange? range) - { - var normalizedRange = NormalizeRange(range); - var category = type switch - { - HookType.Code => HookCategory.Code, - HookType.Block => HookCategory.Block, - _ => throw new ArgumentOutOfRangeException(nameof(type)) - }; - - var registration = new HookRegistration(this, type, category, callback, state); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAdd(engine, type, HookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterMemoryHook(HookType hookType, MemoryAccessType accessType, MemoryHook callback, HookRange? range, object? state) - { - var normalizedRange = NormalizeRange(range); - var registration = new HookRegistration(this, hookType, HookCategory.Memory, callback, state, accessType); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAddMem(engine, hookType, MemHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterEventMemHook(HookType eventTypes, MemoryEventHook callback, HookRange? range, object? state) - { - var normalizedHookTypes = NormalizeEventHookTypes(eventTypes); - var normalizedRange = NormalizeRange(range); - var registration = new HookRegistration(this, normalizedHookTypes, HookCategory.EventMemory, callback, state); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAddEventMem(engine, normalizedHookTypes, EventMemHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterInterruptHook(InterruptHook callback, HookRange? range, object? state) - { - var normalizedRange = NormalizeRange(range); - var registration = new HookRegistration(this, HookType.Interrupt, HookCategory.Interrupt, callback, state); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAddInterrupt(engine, HookType.Interrupt, InterruptHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterInHook(InHook callback, HookRange? range, object? state) - { - var normalizedRange = NormalizeRange(range); - var registration = new HookRegistration(this, HookType.Instruction, HookCategory.In, callback, state); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAddInstructionIn(engine, HookType.Instruction, InstructionInHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, X86InstructionIn, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterOutHook(OutHook callback, HookRange? range, object? state) - { - var normalizedRange = NormalizeRange(range); - var registration = new HookRegistration(this, HookType.Instruction, HookCategory.Out, callback, state); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAddInstructionOut(engine, HookType.Instruction, InstructionOutHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, X86InstructionOut, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterSyscallHook(SyscallHook callback, HookRange? range, object? state) - { - var normalizedRange = NormalizeRange(range); - var registration = new HookRegistration(this, HookType.Instruction, HookCategory.Syscall, callback, state); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAddInstructionSyscall(engine, HookType.Instruction, SyscallHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, X86InstructionSyscall, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterInvalidInstructionHook(InvalidInstructionHook callback, HookRange? range, object? state) - { - var normalizedRange = NormalizeRange(range); - var registration = new HookRegistration(this, HookType.InvalidInstruction, HookCategory.InvalidInstruction, callback, state); - return RegisterHookInternal( - registration, - normalizedRange, - (engine, hookRange) => - { - var err = _native.HookAddInvalidInstruction(engine, HookType.InvalidInstruction, InvalidInstructionHookThunk, registration.UserDataPointer, hookRange.Begin, hookRange.End, out var hookId); - return (err, hookId); - }); - } - - private HookHandle RegisterHookInternal(HookRegistration registration, HookRange range, Func registrar) - { - EnsureNotDisposed(); - - var engine = EngineHandle; - var (error, handle) = registrar(engine, range); - if (error != 0) - { - registration.Dispose(); - throw new UnicornHookException((ErrorCode)error, "uc_hook_add"); - } - - registration.SetHandle(new HookHandle(handle)); - AddRegistration(registration); - return registration.Handle; - } - - private static HookRange NormalizeRange(HookRange? range) - { - return range ?? HookRange.All; - } - - private bool TryGetRegistration(HookHandle handle, [NotNullWhen(true)] out HookRegistration? registration) - { - if (!handle.IsEmpty) - return _hookRegistry.TryGetValue(handle.Value, out registration); - registration = null; - return false; - } - - private void AddRegistration(HookRegistration registration) - { - _hookRegistry.TryAdd(registration.Handle.Value, registration); - - if (registration.Category != HookCategory.EventMemory) - return; - - foreach (var accessType in GetEventAccessTypes(registration)) - { - var handles = _eventMemRegistrations.GetOrAdd(accessType, _ => new ConcurrentDictionary()); - handles.TryAdd(registration.Handle.Value, 0); - } - } - - private void RemoveEventRegistration(HookRegistration registration) - { - if (registration.Category != HookCategory.EventMemory) - { - return; - } - - foreach (var accessType in GetEventAccessTypes(registration)) - { - if (_eventMemRegistrations.TryGetValue(accessType, out var handles)) - { - handles.TryRemove(registration.Handle.Value, out _); - } - } - } - - private static IEnumerable GetEventAccessTypes(HookRegistration registration) - { - if (registration.AccessType.HasValue) - { - yield return registration.AccessType.Value; - yield break; - } - - if ((registration.Type & HookType.MemReadUnmapped) != 0) - yield return MemoryAccessType.ReadUnmapped; - if ((registration.Type & HookType.MemWriteUnmapped) != 0) - yield return MemoryAccessType.WriteUnmapped; - if ((registration.Type & HookType.MemFetchUnmapped) != 0) - yield return MemoryAccessType.FetchUnmapped; - if ((registration.Type & HookType.MemReadProt) != 0) - yield return MemoryAccessType.ReadProtected; - if ((registration.Type & HookType.MemWriteProt) != 0) - yield return MemoryAccessType.WriteProtected; - if ((registration.Type & HookType.MemFetchProt) != 0) - yield return MemoryAccessType.FetchProtected; - } - - private static HookType NormalizeEventHookTypes(HookType eventTypes) - { - var normalized = eventTypes & EventMemoryHookMask; - if (normalized == 0 || (eventTypes & ~EventMemoryHookMask) != 0) - { - throw new ArgumentOutOfRangeException(nameof(eventTypes), "Event types must consist solely of UC_HOOK_MEM_* flags."); - } - - return normalized; - } - - private void RemoveHookInternal(HookHandle handle, bool throwIfMissing) - { - if (handle.IsEmpty) - { - if (throwIfMissing) - { - throw new ArgumentException("Handle is empty", nameof(handle)); - } - - return; - } - - if (!_hookRegistry.TryRemove(handle.Value, out var registration)) - { - if (throwIfMissing) - { - throw new InvalidOperationException($"Hook {handle} was not registered."); - } - - return; - } - - RemoveEventRegistration(registration); - - var engine = EngineHandle; - if (engine != IntPtr.Zero) - { - var err = _native.HookDel(engine, registration.Handle.Value); - if (err != 0) - { - registration.Dispose(); - throw new UnicornHookException((ErrorCode)err, "uc_hook_del"); - } - } - - registration.Dispose(); - } - - private void ClearHooks() - { - var engine = EngineHandle; - - foreach (var registration in _hookRegistry.Values) - { - if (engine != IntPtr.Zero && !registration.Handle.IsEmpty) - { - _native.HookDel(engine, registration.Handle.Value); - } - - registration.Dispose(); - } - - _hookRegistry.Clear(); - _eventMemRegistrations.Clear(); - } -} \ No newline at end of file diff --git a/UnicornNet/Unicorn.Memory.cs b/UnicornNet/Unicorn.Memory.cs deleted file mode 100644 index 6b237da..0000000 --- a/UnicornNet/Unicorn.Memory.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; - -namespace UnicornNet; - -public partial class Unicorn -{ - /// - /// Map a memory region and return a MemoryRegion helper for managing it - /// - public MemoryRegion MapRegion(ulong address, ulong size, MemoryPermissions permissions) - { - MemMap(address, size, permissions); - return new MemoryRegion(this, address, size, permissions); - } - - public void MemMap(ulong address, ulong size, MemoryPermissions permissions) - { - EnsureNotDisposed(); - var err = _native.MemMap(EngineHandle, address, size, (uint)permissions); - if (err != 0) - { - throw new UnicornMemoryException((ErrorCode)err, "uc_mem_map", address, size); - } - } - - public void MemMap(ulong address, ulong size, uint permissions) - { - MemMap(address, size, (MemoryPermissions)permissions); - } - - public void MemMapPtr(ulong address, ulong size, MemoryPermissions permissions, IntPtr pointer) - { - EnsureNotDisposed(); - if (pointer == IntPtr.Zero) - { - throw new ArgumentException("Pointer cannot be zero.", nameof(pointer)); - } - - var err = _native.MemMapPtr(EngineHandle, address, size, (uint)permissions, pointer); - if (err != 0) - { - throw new UnicornMemoryException((ErrorCode)err, "uc_mem_map_ptr", address, size); - } - } - - public void MemMapPtr(ulong address, ulong size, uint permissions, IntPtr pointer) - { - MemMapPtr(address, size, (MemoryPermissions)permissions, pointer); - } - - public void MemUnmap(ulong address, ulong size) - { - EnsureNotDisposed(); - var err = _native.MemUnmap(EngineHandle, address, size); - if (err != 0) - { - throw new UnicornMemoryException((ErrorCode)err, "uc_mem_unmap", address, size); - } - } - - public void MemProtect(ulong address, ulong size, MemoryPermissions permissions) - { - EnsureNotDisposed(); - var err = _native.MemProtect(EngineHandle, address, size, (uint)permissions); - if (err != 0) - { - throw new UnicornMemoryException((ErrorCode)err, "uc_mem_protect", address, size); - } - } - - public void MemProtect(ulong address, ulong size, uint permissions) - { - MemProtect(address, size, (MemoryPermissions)permissions); - } - - public void MemWrite(ulong address, ReadOnlySpan data) - { - EnsureNotDisposed(); - var err = _native.MemWrite(EngineHandle, address, data); - if (err != 0) - { - throw new UnicornMemoryException((ErrorCode)err, "uc_mem_write", address, (ulong)data.Length); - } - } - - public void MemRead(ulong address, Span buffer) - { - EnsureNotDisposed(); - var err = _native.MemRead(EngineHandle, address, buffer); - if (err != 0) - { - throw new UnicornMemoryException((ErrorCode)err, "uc_mem_read", address, (ulong)buffer.Length); - } - } -} \ No newline at end of file diff --git a/UnicornNet/Unicorn.NativeCallbacks.cs b/UnicornNet/Unicorn.NativeCallbacks.cs deleted file mode 100644 index 47f6422..0000000 --- a/UnicornNet/Unicorn.NativeCallbacks.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace UnicornNet; - -public partial class Unicorn -{ - private static readonly NativeHookCallback HookThunk = OnNativeHook; - private static readonly NativeMemHookCallback MemHookThunk = OnNativeMemHook; - private static readonly NativeEventMemHookCallback EventMemHookThunk = OnNativeEventMemHook; - private static readonly NativeInterruptHookCallback InterruptHookThunk = OnNativeInterruptHook; - private static readonly NativeInstructionInHookCallback InstructionInHookThunk = OnNativeInstructionInHook; - private static readonly NativeInstructionOutHookCallback InstructionOutHookThunk = OnNativeInstructionOutHook; - private static readonly NativeSyscallHookCallback SyscallHookThunk = OnNativeSyscallHook; - private static readonly NativeInvalidInstructionHookCallback InvalidInstructionHookThunk = OnNativeInvalidInstructionHook; - - private static void OnNativeHook(IntPtr engine, ulong address, uint size, IntPtr userData) - { - if (!TryResolveRegistration(userData, out var registration)) - { - return; - } - - registration!.InvokeCode(address, (int)size); - } - - private static void OnNativeMemHook(IntPtr engine, uint accessType, ulong address, int size, long value, IntPtr userData) - { - if (!TryResolveRegistration(userData, out var registration)) - { - return; - } - - registration!.InvokeMemory((MemoryAccessType)accessType, address, size, value); - } - - private static bool OnNativeEventMemHook(IntPtr engine, uint accessType, ulong address, int size, long value, IntPtr userData) - { - return TryResolveRegistration(userData, out var registration) && registration!.InvokeEventMemory((MemoryAccessType)accessType, address, size, value); - } - - private static void OnNativeInterruptHook(IntPtr engine, uint interruptNumber, IntPtr userData) - { - if (!TryResolveRegistration(userData, out var registration)) - { - return; - } - - registration!.InvokeInterrupt(interruptNumber); - } - - private static uint OnNativeInstructionInHook(IntPtr engine, uint port, int size, IntPtr userData) - { - return !TryResolveRegistration(userData, out var registration) ? 0 : registration!.InvokeIn(port, size); - } - - private static void OnNativeInstructionOutHook(IntPtr engine, uint port, int size, uint value, IntPtr userData) - { - if (!TryResolveRegistration(userData, out var registration)) - { - return; - } - - registration!.InvokeOut(port, size, value); - } - - private static void OnNativeSyscallHook(IntPtr engine, IntPtr userData) - { - if (!TryResolveRegistration(userData, out var registration)) - { - return; - } - - registration!.InvokeSyscall(); - } - - private static bool OnNativeInvalidInstructionHook(IntPtr engine, IntPtr userData) - { - return TryResolveRegistration(userData, out var registration) && registration!.InvokeInvalidInstruction(); - } - - private static bool TryResolveRegistration(IntPtr userData, out HookRegistration? registration) - { - registration = null; - if (userData == IntPtr.Zero) - { - // Can't log here - we don't have a registration to get the logger from - return false; - } - - var gcHandle = GCHandle.FromIntPtr(userData); - registration = gcHandle.Target as HookRegistration; - - if (registration is null) - { - // GCHandle was valid but target was not a HookRegistration or was collected - // This is a serious issue that indicates a bug or corrupted state - // Unfortunately we can't log here as we don't have access to the logger - return false; - } - - return true; - } -} \ No newline at end of file