mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-15 23:31:07 +00:00
Fix virtual memory allocation and access (#193)
* Fix virtual memory allocation and access * Update test dependency lock file
This commit is contained in:
254
tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs
Normal file
254
tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE.Host;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Memory;
|
||||
|
||||
public sealed class GuestMemoryAllocatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void FreedRangesAreReusedAndCoalesced()
|
||||
{
|
||||
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
||||
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
|
||||
|
||||
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
||||
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
|
||||
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize - 0xC000, 0x1000, out var third));
|
||||
Assert.False(memory.TryAllocateGuestMemory(1, 1, out _));
|
||||
|
||||
Assert.True(memory.TryFreeGuestMemory(second));
|
||||
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var reused));
|
||||
Assert.Equal(second, reused);
|
||||
|
||||
Assert.True(memory.TryFreeGuestMemory(first));
|
||||
Assert.True(memory.TryFreeGuestMemory(reused));
|
||||
Assert.True(memory.TryFreeGuestMemory(third));
|
||||
Assert.False(memory.TryFreeGuestMemory(third));
|
||||
|
||||
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize, 0x1000, out var coalesced));
|
||||
Assert.Equal(first, coalesced);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SegmentProtectionIsAppliedInContiguousRuns()
|
||||
{
|
||||
const ulong pageSize = 0x1000;
|
||||
using var host = new RecordingHostMemory(3 * pageSize);
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
|
||||
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Read);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(host.Address, 3 * pageSize, HostPageProtection.ReadWrite),
|
||||
(host.Address, 3 * pageSize, HostPageProtection.ReadOnly),
|
||||
],
|
||||
host.ProtectionCalls);
|
||||
|
||||
host.ProtectionCalls.Clear();
|
||||
memory.Map(host.Address + pageSize, pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Write);
|
||||
host.ProtectionCalls.Clear();
|
||||
|
||||
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Execute);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(host.Address, 3 * pageSize, HostPageProtection.ReadWriteExecute),
|
||||
(host.Address, pageSize, HostPageProtection.ReadExecute),
|
||||
(host.Address + pageSize, pageSize, HostPageProtection.ReadWriteExecute),
|
||||
(host.Address + (2 * pageSize), pageSize, HostPageProtection.ReadExecute),
|
||||
],
|
||||
host.ProtectionCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public unsafe void GetPointerCommitsLazyPageBeforeReturningIt()
|
||||
{
|
||||
const ulong address = 0x00005000_0000_0000;
|
||||
const ulong pageSize = 0x1000;
|
||||
using var host = new LazyHostMemory(address);
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
memory.AllocateAt(address, (4UL << 30) + pageSize, executable: false, allowAlternative: false);
|
||||
host.CommitCalls.Clear();
|
||||
|
||||
var pointer = memory.GetPointer(address + 0x123);
|
||||
|
||||
Assert.Equal(address + 0x123, (ulong)pointer);
|
||||
Assert.Equal([(address, pageSize, HostPageProtection.ReadWrite)], host.CommitCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public unsafe void GetPointerReturnsNullWhenLazyCommitFails()
|
||||
{
|
||||
const ulong address = 0x00005000_0000_0000;
|
||||
using var host = new LazyHostMemory(address);
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
memory.AllocateAt(address, (4UL << 30) + 0x1000, executable: false, allowAlternative: false);
|
||||
host.CommitCalls.Clear();
|
||||
host.CommitSucceeds = false;
|
||||
|
||||
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
||||
}
|
||||
|
||||
private sealed class FakeHostMemory : IHostMemory
|
||||
{
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
Allocate(desiredAddress, size, protection);
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
||||
|
||||
public bool Free(ulong address) => true;
|
||||
|
||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingHostMemory : IHostMemory, IDisposable
|
||||
{
|
||||
private readonly nint _allocation;
|
||||
private bool _freed;
|
||||
|
||||
public RecordingHostMemory(ulong size)
|
||||
{
|
||||
_allocation = System.Runtime.InteropServices.Marshal.AllocHGlobal(checked((nint)(size + 0xFFF)));
|
||||
Address = (unchecked((ulong)_allocation) + 0xFFF) & ~0xFFFUL;
|
||||
}
|
||||
|
||||
public ulong Address { get; }
|
||||
|
||||
public List<(ulong Address, ulong Size, HostPageProtection Protection)> ProtectionCalls { get; } = [];
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
desiredAddress == Address ? Address : 0;
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
||||
|
||||
public bool Free(ulong address)
|
||||
{
|
||||
if (address != Address || _freed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
||||
_freed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
ProtectionCalls.Add((address, size, protection));
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_freed)
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
||||
_freed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
||||
{
|
||||
public bool CommitSucceeds { get; set; } = true;
|
||||
|
||||
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
desiredAddress == address ? address : 0;
|
||||
|
||||
public bool Commit(ulong commitAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
CommitCalls.Add((commitAddress, size, protection));
|
||||
return CommitSucceeds;
|
||||
}
|
||||
|
||||
public bool Free(ulong freeAddress) => freeAddress == address;
|
||||
|
||||
public bool Protect(ulong protectAddress, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong protectAddress, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong queryAddress, out HostRegionInfo info)
|
||||
{
|
||||
var pageAddress = queryAddress & ~0xFFFUL;
|
||||
info = new HostRegionInfo(
|
||||
pageAddress,
|
||||
address,
|
||||
0x1000,
|
||||
HostRegionState.Reserved,
|
||||
0,
|
||||
HostPageProtection.NoAccess,
|
||||
0,
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong flushAddress, ulong size)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
87
tests/SharpEmu.Libs.Tests/Memory/VirtualMemoryTests.cs
Normal file
87
tests/SharpEmu.Libs.Tests/Memory/VirtualMemoryTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Memory;
|
||||
|
||||
public sealed class VirtualMemoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void OutOfOrderMappingsRemainSortedAndResolveAtBoundaries()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
memory.Map(0x3000, 0x100, 0, [3], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
memory.Map(0x1000, 0x100, 0, [1], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
memory.Map(0x2000, 0x100, 0, [2], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
|
||||
Assert.Equal([0x1000UL, 0x2000UL, 0x3000UL], memory.SnapshotRegions().Select(region => region.VirtualAddress));
|
||||
|
||||
Span<byte> value = stackalloc byte[1];
|
||||
Assert.True(memory.TryRead(0x1000, value));
|
||||
Assert.Equal(1, value[0]);
|
||||
Assert.True(memory.TryRead(0x2000, value));
|
||||
Assert.Equal(2, value[0]);
|
||||
Assert.True(memory.TryRead(0x3000, value));
|
||||
Assert.Equal(3, value[0]);
|
||||
Assert.False(memory.TryRead(0x1100, value));
|
||||
Assert.False(memory.TryRead(0x0FFF, value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MappingRejectsOverlapWithEitherNeighbor()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
memory.Map(0x2000, 0x100, 0, [], ProgramHeaderFlags.Read);
|
||||
memory.Map(0x4000, 0x100, 0, [], ProgramHeaderFlags.Read);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
memory.Map(0x1FFF, 2, 0, [], ProgramHeaderFlags.Read));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
memory.Map(0x3FFF, 2, 0, [], ProgramHeaderFlags.Read));
|
||||
|
||||
memory.Map(0x2100, 0x1F00, 0, [], ProgramHeaderFlags.Read);
|
||||
Assert.Equal(3, memory.SnapshotRegions().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAndWriteSpanAdjacentRegions()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
const ProgramHeaderFlags protection = ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
|
||||
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], protection);
|
||||
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], protection);
|
||||
|
||||
Span<byte> read = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(0x1002, read));
|
||||
Assert.Equal([3, 4, 5, 6], read.ToArray());
|
||||
|
||||
Assert.True(memory.TryWrite(0x1002, [9, 10, 11, 12]));
|
||||
Assert.True(memory.TryRead(0x1000, read));
|
||||
Assert.Equal([1, 2, 9, 10], read.ToArray());
|
||||
Assert.True(memory.TryRead(0x1004, read));
|
||||
Assert.Equal([11, 12, 7, 8], read.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccessRequiresPermissionAcrossEntireRange()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], ProgramHeaderFlags.Read);
|
||||
memory.Map(0x2000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Execute);
|
||||
memory.Map(0x3000, 4, 0, [], ProgramHeaderFlags.Write);
|
||||
|
||||
Assert.False(memory.TryWrite(0x1002, [9, 9, 9, 9]));
|
||||
Span<byte> unchanged = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(0x1000, unchanged));
|
||||
Assert.Equal([1, 2, 3, 4], unchanged.ToArray());
|
||||
|
||||
Assert.False(memory.TryRead(0x2000, unchanged));
|
||||
Assert.False(memory.TryWrite(0x2000, [9]));
|
||||
Assert.False(memory.TryRead(0x3000, unchanged));
|
||||
Assert.True(memory.TryWrite(0x3000, [9]));
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -149,6 +149,15 @@
|
||||
"xunit.extensibility.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Iced": "[1.21.0, )",
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"SharpEmu.Libs": "[1.0.0, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
@@ -168,6 +177,12 @@
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
Reference in New Issue
Block a user